diff options
Diffstat (limited to 'src/qdoc/qdoc/src/qdoc')
138 files changed, 46905 insertions, 0 deletions
diff --git a/src/qdoc/qdoc/src/qdoc/access.h b/src/qdoc/qdoc/src/qdoc/access.h new file mode 100644 index 000000000..96cad686b --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/access.h @@ -0,0 +1,15 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <QtCore/qglobal.h> + +#ifndef ACCESS_H +#define ACCESS_H + +QT_BEGIN_NAMESPACE + +enum class Access : unsigned char { Public, Protected, Private }; + +QT_END_NAMESPACE + +#endif // ACCESS_H diff --git a/src/qdoc/qdoc/src/qdoc/aggregate.cpp b/src/qdoc/qdoc/src/qdoc/aggregate.cpp new file mode 100644 index 000000000..bf210ba17 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/aggregate.cpp @@ -0,0 +1,762 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "aggregate.h" + +#include "functionnode.h" +#include "parameters.h" +#include "typedefnode.h" +#include "qdocdatabase.h" +#include "qmlpropertynode.h" +#include "qmltypenode.h" +#include "sharedcommentnode.h" +#include <vector> + +using namespace Qt::Literals::StringLiterals; + +QT_BEGIN_NAMESPACE + +/*! + \class Aggregate + */ + +/*! \fn Aggregate::Aggregate(NodeType type, Aggregate *parent, const QString &name) + The constructor should never be called directly. It is only called + by the constructors of subclasses of Aggregate. Those constructors + pass the node \a type they want to create, the \a parent of the new + node, and its \a name. + */ + +/*! + Recursively set all non-related members in the list of children to + \nullptr, after which each aggregate can safely delete all children + in their list. Aggregate's destructor calls this only on the root + namespace node. + */ +void Aggregate::dropNonRelatedMembers() +{ + for (auto &child : m_children) { + if (!child) + continue; + if (child->parent() != this) + child = nullptr; + else if (child->isAggregate()) + static_cast<Aggregate*>(child)->dropNonRelatedMembers(); + } +} + +/*! + Destroys this Aggregate; deletes each child. + */ +Aggregate::~Aggregate() +{ + // If this is the root, clear non-related children first + if (isNamespace() && name().isEmpty()) + dropNonRelatedMembers(); + + m_enumChildren.clear(); + m_nonfunctionMap.clear(); + m_functionMap.clear(); + qDeleteAll(m_children.begin(), m_children.end()); + m_children.clear(); +} + +/*! + If \a genus is \c{Node::DontCare}, find the first node in + this node's child list that has the given \a name. If this + node is a QML type, be sure to also look in the children + of its property group nodes. Return the matching node or \c nullptr. + + If \a genus is either \c{Node::CPP} or \c {Node::QML}, then + find all this node's children that have the given \a name, + and return the one that satisfies the \a genus requirement. + */ +Node *Aggregate::findChildNode(const QString &name, Node::Genus genus, int findFlags) const +{ + if (genus == Node::DontCare) { + Node *node = m_nonfunctionMap.value(name); + if (node) + return node; + } else { + const NodeList &nodes = m_nonfunctionMap.values(name); + for (auto *node : nodes) { + if (genus & node->genus()) { + if (findFlags & TypesOnly) { + if (!node->isTypedef() && !node->isClassNode() + && !node->isQmlType() && !node->isEnumType()) + continue; + } else if (findFlags & IgnoreModules && node->isModule()) + continue; + return node; + } + } + } + if (genus != Node::DontCare && !(genus & this->genus())) + return nullptr; + + auto it = m_functionMap.find(name); + return it != m_functionMap.end() ? (*(*it).begin()) : nullptr; +} + +/*! + Find all the child nodes of this node that are named + \a name and return them in \a nodes. + */ +void Aggregate::findChildren(const QString &name, NodeVector &nodes) const +{ + nodes.clear(); + const auto &functions = m_functionMap.value(name); + nodes.reserve(functions.size() + m_nonfunctionMap.count(name)); + for (auto f : functions) + nodes.emplace_back(f); + auto [it, end] = m_nonfunctionMap.equal_range(name); + while (it != end) { + nodes.emplace_back(*it); + ++it; + } +} + +/*! + This function searches for a child node of this Aggregate, + such that the child node has the spacified \a name and the + function \a isMatch returns true for the node. The function + passed must be one of the isXxx() functions in class Node + that tests the node type. + */ +Node *Aggregate::findNonfunctionChild(const QString &name, bool (Node::*isMatch)() const) +{ + const NodeList &nodes = m_nonfunctionMap.values(name); + for (auto *node : nodes) { + if ((node->*(isMatch))()) + return node; + } + return nullptr; +} + +/*! + Find a function node that is a child of this node, such that + the function node has the specified \a name and \a parameters. + If \a parameters is empty but no matching function is found + that has no parameters, return the first non-internal primary + function or overload, whether it has parameters or not. + + \sa normalizeOverloads() + */ +FunctionNode *Aggregate::findFunctionChild(const QString &name, const Parameters ¶meters) +{ + auto map_it = m_functionMap.find(name); + if (map_it == m_functionMap.end()) + return nullptr; + + auto match_it = std::find_if((*map_it).begin(), (*map_it).end(), + [¶meters](const FunctionNode *fn) { + if (fn->isInternal()) + return false; + if (parameters.count() != fn->parameters().count()) + return false; + for (int i = 0; i < parameters.count(); ++i) + if (parameters.at(i).type() != fn->parameters().at(i).type()) + return false; + return true; + }); + + if (match_it != (*map_it).end()) + return *match_it; + + // Assumes that overloads are already normalized; i.e, if there's + // an active function, it'll be found at the start of the list. + auto *fn = (*(*map_it).begin()); + return (parameters.isEmpty() && !fn->isInternal()) ? fn : nullptr; +} + +/*! + Returns the function node that is a child of this node, such + that the function described has the same name and signature + as the function described by the function node \a clone. + + Returns \nullptr if no matching function was found. + */ +FunctionNode *Aggregate::findFunctionChild(const FunctionNode *clone) +{ + auto funcs_it = m_functionMap.find(clone->name()); + if (funcs_it == m_functionMap.end()) + return nullptr; + + auto func_it = std::find_if((*funcs_it).begin(), (*funcs_it).end(), + [clone](const FunctionNode *fn) { + return compare(clone, fn) == 0; + }); + + return func_it != (*funcs_it).end() ? *func_it : nullptr; +} + +/*! + Mark all child nodes that have no documentation as having + private access and internal status. qdoc will then ignore + them for documentation purposes. + */ +void Aggregate::markUndocumentedChildrenInternal() +{ + for (auto *child : std::as_const(m_children)) { + if (!child->hasDoc() && !child->isDontDocument()) { + if (!child->docMustBeGenerated()) { + if (child->isFunction()) { + if (static_cast<FunctionNode *>(child)->hasAssociatedProperties()) + continue; + } else if (child->isTypedef()) { + if (static_cast<TypedefNode *>(child)->hasAssociatedEnum()) + continue; + } + child->setAccess(Access::Private); + child->setStatus(Node::Internal); + } + } + if (child->isAggregate()) { + static_cast<Aggregate *>(child)->markUndocumentedChildrenInternal(); + } + } +} + +/*! + Adopts each non-aggregate C++ node (function/macro, typedef, enum, variable, + or a shared comment node with genus Node::CPP) in the global scope to the + aggregate specified in the node's documentation using the \\relates command. + + If the target Aggregate is not found in the primary tree, creates a new + ProxyNode to use as the parent. +*/ +void Aggregate::resolveRelates() +{ + Q_ASSERT(name().isEmpty()); // Must be called on the root namespace + auto *database = QDocDatabase::qdocDB(); + + for (auto *node : m_children) { + if (node->isRelatedNonmember() || node->isAggregate()) + continue; + if (node->genus() != Node::CPP) + continue; + + const auto &relates_args = node->doc().metaCommandArgs("relates"_L1); + if (relates_args.isEmpty()) + continue; + + auto *aggregate = database->findRelatesNode(relates_args[0].first.split("::"_L1)); + if (!aggregate) + aggregate = new ProxyNode(this, relates_args[0].first); + else if (node->parent() == aggregate) + continue; + + aggregate->adoptChild(node); + node->setRelatedNonmember(true); + } +} + +/*! + Sorts the lists of overloads in the function map and assigns overload + numbers. + + For sorting, active functions take precedence over internal ones, as well + as ones marked as \\overload - the latter ones typically do not contain + full documentation, so selecting them as the \e primary function + would cause unnecessary warnings to be generated. + + Otherwise, the order is set as determined by FunctionNode::compare(). + */ +void Aggregate::normalizeOverloads() +{ + for (auto map_it = m_functionMap.begin(); map_it != m_functionMap.end(); ++map_it) { + if ((*map_it).size() > 1) { + std::sort((*map_it).begin(), (*map_it).end(), + [](const FunctionNode *f1, const FunctionNode *f2) -> bool { + if (f1->isInternal() != f2->isInternal()) + return f2->isInternal(); + if (f1->isOverload() != f2->isOverload()) + return f2->isOverload(); + // Prioritize documented over undocumented + if (f1->hasDoc() != f2->hasDoc()) + return f1->hasDoc(); + return (compare(f1, f2) < 0); + }); + // Set overload numbers + signed short n{0}; + for (auto *fn : (*map_it)) + fn->setOverloadNumber(n++); + } + } + + for (auto *node : std::as_const(m_children)) { + if (node->isAggregate()) + static_cast<Aggregate *>(node)->normalizeOverloads(); + } +} + +/*! + Returns a const reference to the list of child nodes of this + aggregate that are not function nodes. Duplicate nodes are + removed from the list. + */ +const NodeList &Aggregate::nonfunctionList() +{ + m_nonfunctionList = m_nonfunctionMap.values(); + std::sort(m_nonfunctionList.begin(), m_nonfunctionList.end(), Node::nodeNameLessThan); + m_nonfunctionList.erase(std::unique(m_nonfunctionList.begin(), m_nonfunctionList.end()), + m_nonfunctionList.end()); + return m_nonfunctionList; +} + +/*! \fn bool Aggregate::isAggregate() const + Returns \c true because this node is an instance of Aggregate, + which means it can have children. + */ + +/*! + Finds the enum type node that has \a enumValue as one of + its enum values and returns a pointer to it. Returns 0 if + no enum type node is found that has \a enumValue as one + of its values. + */ +const EnumNode *Aggregate::findEnumNodeForValue(const QString &enumValue) const +{ + for (const auto *node : m_enumChildren) { + const auto *en = static_cast<const EnumNode *>(node); + if (en->hasItem(enumValue)) + return en; + } + return nullptr; +} + +/*! + Adds the \a child to this node's child map using \a title + as the key. The \a child is not added to the child list + again, because it is presumed to already be there. We just + want to be able to find the child by its \a title. + */ +void Aggregate::addChildByTitle(Node *child, const QString &title) +{ + m_nonfunctionMap.insert(title, child); +} + +/*! + Adds the \a child to this node's child list and sets the child's + parent pointer to this Aggregate. It then mounts the child with + mountChild(). + + The \a child is then added to this Aggregate's searchable maps + and lists. + + \note This function does not test the child's parent pointer + for null before changing it. If the child's parent pointer + is not null, then it is being reparented. The child becomes + a child of this Aggregate, but it also remains a child of + the Aggregate that is it's old parent. But the child will + only have one parent, and it will be this Aggregate. The is + because of the \c relates command. + + \sa mountChild(), dismountChild() + */ +void Aggregate::addChild(Node *child) +{ + m_children.append(child); + child->setParent(this); + child->setUrl(QString()); + child->setIndexNodeFlag(isIndexNode()); + + if (child->isFunction()) { + m_functionMap[child->name()].emplace_back(static_cast<FunctionNode *>(child)); + } else if (!child->name().isEmpty()) { + m_nonfunctionMap.insert(child->name(), child); + if (child->isEnumType()) + m_enumChildren.append(child); + } +} + +/*! + This Aggregate becomes the adoptive parent of \a child. The + \a child knows this Aggregate as its parent, but its former + parent continues to have pointers to the child in its child + list and in its searchable data structures. But the child is + also added to the child list and searchable data structures + of this Aggregate. + */ +void Aggregate::adoptChild(Node *child) +{ + if (child->parent() != this) { + m_children.append(child); + child->setParent(this); + if (child->isFunction()) { + m_functionMap[child->name()].emplace_back(static_cast<FunctionNode *>(child)); + } else if (!child->name().isEmpty()) { + m_nonfunctionMap.insert(child->name(), child); + if (child->isEnumType()) + m_enumChildren.append(child); + } + if (child->isSharedCommentNode()) { + auto *scn = static_cast<SharedCommentNode *>(child); + for (Node *n : scn->collective()) + adoptChild(n); + } + } +} + +/*! + If this node has a child that is a QML property named \a n, return a + pointer to that child. Otherwise, return \nullptr. + */ +QmlPropertyNode *Aggregate::hasQmlProperty(const QString &n) const +{ + NodeType goal = Node::QmlProperty; + for (auto *child : std::as_const(m_children)) { + if (child->nodeType() == goal) { + if (child->name() == n) + return static_cast<QmlPropertyNode *>(child); + } + } + return nullptr; +} + +/*! + If this node has a child that is a QML property named \a n and that + also matches \a attached, return a pointer to that child. + */ +QmlPropertyNode *Aggregate::hasQmlProperty(const QString &n, bool attached) const +{ + NodeType goal = Node::QmlProperty; + for (auto *child : std::as_const(m_children)) { + if (child->nodeType() == goal) { + if (child->name() == n && child->isAttached() == attached) + return static_cast<QmlPropertyNode *>(child); + } + } + return nullptr; +} + +/*! + Returns \c true if this aggregate has multiple function + overloads matching the name of \a fn. + + \note Assumes \a fn is a member of this aggregate. +*/ +bool Aggregate::hasOverloads(const FunctionNode *fn) const +{ + auto it = m_functionMap.find(fn->name()); + return !(it == m_functionMap.end()) && (it.value().size() > 1); +} + +/* + When deciding whether to include a function in the function + index, if the function is marked private, don't include it. + If the function is marked obsolete, don't include it. If the + function is marked internal, don't include it. Or if the + function is a destructor or any kind of constructor, don't + include it. Otherwise include it. + */ +static bool keep(FunctionNode *fn) +{ + if (fn->isPrivate() || fn->isDeprecated() || fn->isInternal() || fn->isSomeCtor() || fn->isDtor()) + return false; + return true; +} + +/*! + Insert all functions declared in this aggregate into the + \a functionIndex. Call the function recursively for each + child that is an aggregate. + + Only include functions that are in the public API and + that are not constructors or destructors. + */ +void Aggregate::findAllFunctions(NodeMapMap &functionIndex) +{ + for (auto functions : m_functionMap) { + std::for_each(functions.begin(), functions.end(), + [&functionIndex](FunctionNode *fn) { + if (keep(fn)) + functionIndex[fn->name()].insert(fn->parent()->fullDocumentName(), fn); + } + ); + } + + for (Node *node : std::as_const(m_children)) { + if (node->isAggregate() && !node->isPrivate() && !node->isDontDocument()) + static_cast<Aggregate *>(node)->findAllFunctions(functionIndex); + } +} + +/*! + For each child of this node, if the child is a namespace node, + insert the child into the \a namespaces multimap. If the child + is an aggregate, call this function recursively for that child. + + When the function called with the root node of a tree, it finds + all the namespace nodes in that tree and inserts them into the + \a namespaces multimap. + + The root node of a tree is a namespace, but it has no name, so + it is not inserted into the map. So, if this function is called + for each tree in the qdoc database, it finds all the namespace + nodes in the database. + */ +void Aggregate::findAllNamespaces(NodeMultiMap &namespaces) +{ + for (auto *node : std::as_const(m_children)) { + if (node->isAggregate() && !node->isPrivate()) { + if (node->isNamespace() && !node->name().isEmpty()) + namespaces.insert(node->name(), node); + static_cast<Aggregate *>(node)->findAllNamespaces(namespaces); + } + } +} + +/*! + Returns true if this aggregate contains at least one child + that is marked obsolete. Otherwise returns false. + */ +bool Aggregate::hasObsoleteMembers() const +{ + for (const auto *node : m_children) + if (!node->isPrivate() && node->isDeprecated()) { + if (node->isFunction() || node->isProperty() || node->isEnumType() || node->isTypedef() + || node->isTypeAlias() || node->isVariable() || node->isQmlProperty()) + return true; + } + return false; +} + +/*! + Finds all the obsolete C++ classes and QML types in this + aggregate and all the C++ classes and QML types with obsolete + members, and inserts them into maps used elsewhere for + generating documentation. + */ +void Aggregate::findAllObsoleteThings() +{ + for (auto *node : std::as_const(m_children)) { + if (!node->isPrivate()) { + if (node->isDeprecated()) { + if (node->isClassNode()) + QDocDatabase::obsoleteClasses().insert(node->qualifyCppName(), node); + else if (node->isQmlType()) + QDocDatabase::obsoleteQmlTypes().insert(node->qualifyQmlName(), node); + } else if (node->isClassNode()) { + auto *a = static_cast<Aggregate *>(node); + if (a->hasObsoleteMembers()) + QDocDatabase::classesWithObsoleteMembers().insert(node->qualifyCppName(), node); + } else if (node->isQmlType()) { + auto *a = static_cast<Aggregate *>(node); + if (a->hasObsoleteMembers()) + QDocDatabase::qmlTypesWithObsoleteMembers().insert(node->qualifyQmlName(), + node); + } else if (node->isAggregate()) { + static_cast<Aggregate *>(node)->findAllObsoleteThings(); + } + } + } +} + +/*! + Finds all the C++ classes, QML types, QML basic types, and examples + in this aggregate and inserts them into appropriate maps for later + use in generating documentation. + */ +void Aggregate::findAllClasses() +{ + for (auto *node : std::as_const(m_children)) { + if (!node->isPrivate() && !node->isInternal() && !node->isDontDocument() + && node->tree()->camelCaseModuleName() != QString("QDoc")) { + if (node->isClassNode()) { + QDocDatabase::cppClasses().insert(node->qualifyCppName().toLower(), node); + } else if (node->isQmlType()) { + QString name = node->name().toLower(); + QDocDatabase::qmlTypes().insert(name, node); + // also add to the QML basic type map + if (node->isQmlBasicType()) + QDocDatabase::qmlBasicTypes().insert(name, node); + } else if (node->isExample()) { + // use the module index title as key for the example map + QString title = node->tree()->indexTitle(); + if (!QDocDatabase::examples().contains(title, node)) + QDocDatabase::examples().insert(title, node); + } else if (node->isAggregate()) { + static_cast<Aggregate *>(node)->findAllClasses(); + } + } + } +} + +/*! + Find all the attribution pages in this node and insert them + into \a attributions. + */ +void Aggregate::findAllAttributions(NodeMultiMap &attributions) +{ + for (auto *node : std::as_const(m_children)) { + if (!node->isPrivate()) { + if (node->isPageNode() && static_cast<PageNode*>(node)->isAttribution()) + attributions.insert(node->tree()->indexTitle(), node); + else if (node->isAggregate()) + static_cast<Aggregate *>(node)->findAllAttributions(attributions); + } + } +} + +/*! + Finds all the nodes in this node where a \e{since} command appeared + in the qdoc comment and sorts them into maps according to the kind + of node. + + This function is used for generating the "New Classes... in x.y" + section on the \e{What's New in Qt x.y} page. + */ +void Aggregate::findAllSince() +{ + for (auto *node : std::as_const(m_children)) { + if (node->isRelatedNonmember() && node->parent() != this) + continue; + QString sinceString = node->since(); + // Insert a new entry into each map for each new since string found. + if (node->isInAPI() && !sinceString.isEmpty()) { + // operator[] will insert a default-constructed value into the + // map if key is not found, which is what we want here. + auto &nsmap = QDocDatabase::newSinceMaps()[sinceString]; + auto &ncmap = QDocDatabase::newClassMaps()[sinceString]; + auto &nqcmap = QDocDatabase::newQmlTypeMaps()[sinceString]; + + if (node->isFunction()) { + // Insert functions into the general since map. + auto *fn = static_cast<FunctionNode *>(node); + if (!fn->isDeprecated() && !fn->isSomeCtor() && !fn->isDtor()) + nsmap.insert(fn->name(), fn); + } else if (node->isClassNode()) { + // Insert classes into the since and class maps. + QString name = node->qualifyWithParentName(); + nsmap.insert(name, node); + ncmap.insert(name, node); + } else if (node->isQmlType()) { + // Insert QML elements into the since and element maps. + QString name = node->qualifyWithParentName(); + nsmap.insert(name, node); + nqcmap.insert(name, node); + } else if (node->isQmlProperty()) { + // Insert QML properties into the since map. + nsmap.insert(node->name(), node); + } else { + // Insert external documents into the general since map. + QString name = node->qualifyWithParentName(); + nsmap.insert(name, node); + } + } + // Enum values - a special case as EnumItem is not a Node subclass + if (node->isInAPI() && node->isEnumType()) { + for (const auto &val : static_cast<EnumNode *>(node)->items()) { + sinceString = val.since(); + if (sinceString.isEmpty()) + continue; + // Insert to enum value map + QDocDatabase::newEnumValueMaps()[sinceString].insert( + node->name() + "::" + val.name(), node); + // Ugly hack: Insert into general map with an empty key - + // we need something in there to mark the corresponding + // section populated. See Sections class constructor. + QDocDatabase::newSinceMaps()[sinceString].replace(QString(), node); + } + } + + // Recursively find child nodes with since commands. + if (node->isAggregate()) + static_cast<Aggregate *>(node)->findAllSince(); + } +} + +/*! + Resolves the inheritance information for all QML type children + of this aggregate. +*/ +void Aggregate::resolveQmlInheritance() +{ + NodeMap previousSearches; + for (auto *child : std::as_const(m_children)) { + if (!child->isQmlType()) + continue; + static_cast<QmlTypeNode *>(child)->resolveInheritance(previousSearches); + } +} + +/*! + Returns a word representing the kind of Aggregate this node is. + Currently only works for class, struct, and union, but it can + easily be extended. If \a cap is true, the word is capitalised. + */ +QString Aggregate::typeWord(bool cap) const +{ + if (cap) { + switch (nodeType()) { + case Node::Class: + return QLatin1String("Class"); + case Node::Struct: + return QLatin1String("Struct"); + case Node::Union: + return QLatin1String("Union"); + default: + break; + } + } else { + switch (nodeType()) { + case Node::Class: + return QLatin1String("class"); + case Node::Struct: + return QLatin1String("struct"); + case Node::Union: + return QLatin1String("union"); + default: + break; + } + } + return QString(); +} + +/*! \fn int Aggregate::count() const + Returns the number of children in the child list. + */ + +/*! \fn const NodeList &Aggregate::childNodes() const + Returns a const reference to the child list. + */ + +/*! \fn NodeList::ConstIterator Aggregate::constBegin() const + Returns a const iterator pointing at the beginning of the child list. + */ + +/*! \fn NodeList::ConstIterator Aggregate::constEnd() const + Returns a const iterator pointing at the end of the child list. + */ + +/*! \fn QmlTypeNode *Aggregate::qmlBaseNode() const + If this Aggregate is a QmlTypeNode, this function returns a pointer to + the QmlTypeNode that is its base type. Otherwise it returns \c nullptr. + A QmlTypeNode doesn't always have a base type, so even when this Aggregate + is aQmlTypeNode, the pointer returned can be \c nullptr. + */ + +/*! \fn FunctionMap &Aggregate::functionMap() + Returns a reference to this Aggregate's function map, which + is a map of all the children of this Aggregate that are + FunctionNodes. + */ + +/*! \fn void Aggregate::appendToRelatedByProxy(const NodeList &t) + Appends the list of node pointers to the list of elements that are + related to this Aggregate but are documented in a different module. + + \sa relatedByProxy() + */ + +/*! \fn NodeList &Aggregate::relatedByProxy() + Returns a reference to a list of node pointers where each element + points to a node in an index file for some other module, such that + whatever the node represents was documented in that other module, + but it is related to this Aggregate, so when the documentation for + this Aggregate is written, it will contain links to elements in the + other module. + */ + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/aggregate.h b/src/qdoc/qdoc/src/qdoc/aggregate.h new file mode 100644 index 000000000..a02633e04 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/aggregate.h @@ -0,0 +1,118 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef AGGREGATE_H +#define AGGREGATE_H + +#include "pagenode.h" + +#include <optional> +#include <vector> + +#include <QtCore/qglobal.h> +#include <QtCore/qstringlist.h> + +QT_BEGIN_NAMESPACE + +class FunctionNode; +class QmlTypeNode; +class QmlPropertyNode; + +class Aggregate : public PageNode +{ +public: + using FunctionMap = QMap<QString, std::vector<FunctionNode*>>; + + [[nodiscard]] Node *findChildNode(const QString &name, Node::Genus genus, + int findFlags = 0) const; + Node *findNonfunctionChild(const QString &name, bool (Node::*)() const); + void findChildren(const QString &name, NodeVector &nodes) const; + FunctionNode *findFunctionChild(const QString &name, const Parameters ¶meters); + FunctionNode *findFunctionChild(const FunctionNode *clone); + + void resolveRelates(); + void normalizeOverloads(); + void markUndocumentedChildrenInternal(); + + [[nodiscard]] bool isAggregate() const override { return true; } + [[nodiscard]] const EnumNode *findEnumNodeForValue(const QString &enumValue) const; + + [[nodiscard]] qsizetype count() const { return m_children.size(); } + [[nodiscard]] const NodeList &childNodes() const { return m_children; } + const NodeList &nonfunctionList(); + [[nodiscard]] NodeList::ConstIterator constBegin() const { return m_children.constBegin(); } + [[nodiscard]] NodeList::ConstIterator constEnd() const { return m_children.constEnd(); } + + inline void setIncludeFile(const QString& include) { m_includeFile.emplace(include); } + // REMARK: Albeit not enforced at the API boundaries, + // downstream-user can assume that if there is a QString that was + // set, then that string is not empty. + [[nodiscard]] inline const std::optional<QString>& includeFile() const { return m_includeFile; } + + [[nodiscard]] QmlPropertyNode *hasQmlProperty(const QString &) const; + [[nodiscard]] QmlPropertyNode *hasQmlProperty(const QString &, bool attached) const; + virtual QmlTypeNode *qmlBaseNode() const { return nullptr; } + void addChildByTitle(Node *child, const QString &title); + void addChild(Node *child); + void adoptChild(Node *child); + + FunctionMap &functionMap() { return m_functionMap; } + void findAllFunctions(NodeMapMap &functionIndex); + void findAllNamespaces(NodeMultiMap &namespaces); + void findAllAttributions(NodeMultiMap &attributions); + [[nodiscard]] bool hasObsoleteMembers() const; + void findAllObsoleteThings(); + void findAllClasses(); + void findAllSince(); + void resolveQmlInheritance(); + bool hasOverloads(const FunctionNode *fn) const; + void appendToRelatedByProxy(const NodeList &t) { m_relatedByProxy.append(t); } + NodeList &relatedByProxy() { return m_relatedByProxy; } + [[nodiscard]] QString typeWord(bool cap) const; + +protected: + Aggregate(NodeType type, Aggregate *parent, const QString &name) + : PageNode(type, parent, name) {} + ~Aggregate() override; + +private: + friend class Node; + void dropNonRelatedMembers(); + +protected: + NodeList m_children {}; + NodeList m_relatedByProxy {}; + FunctionMap m_functionMap {}; + +private: + // REMARK: The member indicates the name of a file where the + // aggregate can be found from, for example, an header file that + // declares a class. + // For aggregates such as classes we expect this to always be set + // to a non-empty string after the code-parsing phase. + // Indeed, currently, by default, QDoc always generates such a + // string using the name of the aggregate if no include file can + // be propagated from some of the parents. + // + // Nonetheless, we are still forced to make this an optional, as + // this will not be true for all Aggregates. + // + // For example, for namespaces, we don't seem to set an include + // file and indeed doing so wouldn't be particularly meaningful. + // + // It is possible to assume in later code, especially the + // generation phase, that at least some classes of aggregates + // always have a value set here but we should, for the moment, + // still check for the possibility of something not to be there, + // or warn if we decide to ignore that, to be compliant with the + // current interface, whose change would require deep changes to + // QDoc internal structures. + std::optional<QString> m_includeFile{}; + NodeList m_enumChildren {}; + NodeMultiMap m_nonfunctionMap {}; + NodeList m_nonfunctionList {}; +}; + +QT_END_NAMESPACE + +#endif // AGGREGATE_H diff --git a/src/qdoc/qdoc/src/qdoc/atom.cpp b/src/qdoc/qdoc/src/qdoc/atom.cpp new file mode 100644 index 000000000..f887c4ec5 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/atom.cpp @@ -0,0 +1,458 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "atom.h" + +#include "location.h" +#include "qdocdatabase.h" + +#include <QtCore/qregularexpression.h> + +#include <cstdio> + +QT_BEGIN_NAMESPACE + +/*! \class Atom + \brief The Atom class is the fundamental unit for representing + documents internally. + + Atoms have a \i type and are completed by a \i string whose + meaning depends on the \i type. For example, the string + \quotation + \i italic text looks nicer than \bold bold text + \endquotation + is represented by the following atoms: + \quotation + (FormattingLeft, ATOM_FORMATTING_ITALIC) + (String, "italic") + (FormattingRight, ATOM_FORMATTING_ITALIC) + (String, " text is more attractive than ") + (FormattingLeft, ATOM_FORMATTING_BOLD) + (String, "bold") + (FormattingRight, ATOM_FORMATTING_BOLD) + (String, " text") + \endquotation + + \also Text +*/ + +/*! \enum Atom::AtomType + + \value AnnotatedList + \value AutoLink + \value BaseName + \value BriefLeft + \value BriefRight + \value C + \value CaptionLeft + \value CaptionRight + \value Code + \value CodeBad + \value CodeQuoteArgument + \value CodeQuoteCommand + \value DetailsLeft + \value DetailsRight + \value DivLeft + \value DivRight + \value ExampleFileLink + \value ExampleImageLink + \value FormatElse + \value FormatEndif + \value FormatIf + \value FootnoteLeft + \value FootnoteRight + \value FormattingLeft + \value FormattingRight + \value GeneratedList + \value Image + \value ImageText + \value ImportantNote + \value InlineImage + \value Keyword + \value LineBreak + \value Link + \value LinkNode + \value ListLeft + \value ListItemNumber + \value ListTagLeft + \value ListTagRight + \value ListItemLeft + \value ListItemRight + \value ListRight + \value NavAutoLink + \value NavLink + \value Nop + \value Note + \value ParaLeft + \value ParaRight + \value Qml + \value QuotationLeft + \value QuotationRight + \value RawString + \value SectionLeft + \value SectionRight + \value SectionHeadingLeft + \value SectionHeadingRight + \value SidebarLeft + \value SidebarRight + \value SinceList + \value SinceTagLeft + \value SinceTagRight + \value String + \value TableLeft + \value TableRight + \value TableHeaderLeft + \value TableHeaderRight + \value TableRowLeft + \value TableRowRight + \value TableItemLeft + \value TableItemRight + \value TableOfContents + \value Target + \value UnhandledFormat + \value UnknownCommand +*/ + +static const struct +{ + const char *english; + int no; +} atms[] = { { "AnnotatedList", Atom::AnnotatedList }, + { "AutoLink", Atom::AutoLink }, + { "BaseName", Atom::BaseName }, + { "br", Atom::BR }, + { "BriefLeft", Atom::BriefLeft }, + { "BriefRight", Atom::BriefRight }, + { "C", Atom::C }, + { "CaptionLeft", Atom::CaptionLeft }, + { "CaptionRight", Atom::CaptionRight }, + { "Code", Atom::Code }, + { "CodeBad", Atom::CodeBad }, + { "CodeQuoteArgument", Atom::CodeQuoteArgument }, + { "CodeQuoteCommand", Atom::CodeQuoteCommand }, + { "ComparesLeft", Atom::ComparesLeft }, + { "ComparesRight", Atom::ComparesRight }, + { "DetailsLeft", Atom::DetailsLeft }, + { "DetailsRight", Atom::DetailsRight }, + { "DivLeft", Atom::DivLeft }, + { "DivRight", Atom::DivRight }, + { "ExampleFileLink", Atom::ExampleFileLink }, + { "ExampleImageLink", Atom::ExampleImageLink }, + { "FootnoteLeft", Atom::FootnoteLeft }, + { "FootnoteRight", Atom::FootnoteRight }, + { "FormatElse", Atom::FormatElse }, + { "FormatEndif", Atom::FormatEndif }, + { "FormatIf", Atom::FormatIf }, + { "FormattingLeft", Atom::FormattingLeft }, + { "FormattingRight", Atom::FormattingRight }, + { "GeneratedList", Atom::GeneratedList }, + { "hr", Atom::HR }, + { "Image", Atom::Image }, + { "ImageText", Atom::ImageText }, + { "ImportantLeft", Atom::ImportantLeft }, + { "ImportantRight", Atom::ImportantRight }, + { "InlineImage", Atom::InlineImage }, + { "Keyword", Atom::Keyword }, + { "LegaleseLeft", Atom::LegaleseLeft }, + { "LegaleseRight", Atom::LegaleseRight }, + { "LineBreak", Atom::LineBreak }, + { "Link", Atom::Link }, + { "LinkNode", Atom::LinkNode }, + { "ListLeft", Atom::ListLeft }, + { "ListItemNumber", Atom::ListItemNumber }, + { "ListTagLeft", Atom::ListTagLeft }, + { "ListTagRight", Atom::ListTagRight }, + { "ListItemLeft", Atom::ListItemLeft }, + { "ListItemRight", Atom::ListItemRight }, + { "ListRight", Atom::ListRight }, + { "NavAutoLink", Atom::NavAutoLink }, + { "NavLink", Atom::NavLink }, + { "Nop", Atom::Nop }, + { "NoteLeft", Atom::NoteLeft }, + { "NoteRight", Atom::NoteRight }, + { "ParaLeft", Atom::ParaLeft }, + { "ParaRight", Atom::ParaRight }, + { "Qml", Atom::Qml }, + { "QuotationLeft", Atom::QuotationLeft }, + { "QuotationRight", Atom::QuotationRight }, + { "RawString", Atom::RawString }, + { "SectionLeft", Atom::SectionLeft }, + { "SectionRight", Atom::SectionRight }, + { "SectionHeadingLeft", Atom::SectionHeadingLeft }, + { "SectionHeadingRight", Atom::SectionHeadingRight }, + { "SidebarLeft", Atom::SidebarLeft }, + { "SidebarRight", Atom::SidebarRight }, + { "SinceList", Atom::SinceList }, + { "SinceTagLeft", Atom::SinceTagLeft }, + { "SinceTagRight", Atom::SinceTagRight }, + { "SnippetCommand", Atom::SnippetCommand }, + { "SnippetIdentifier", Atom::SnippetIdentifier }, + { "SnippetLocation", Atom::SnippetLocation }, + { "String", Atom::String }, + { "TableLeft", Atom::TableLeft }, + { "TableRight", Atom::TableRight }, + { "TableHeaderLeft", Atom::TableHeaderLeft }, + { "TableHeaderRight", Atom::TableHeaderRight }, + { "TableRowLeft", Atom::TableRowLeft }, + { "TableRowRight", Atom::TableRowRight }, + { "TableItemLeft", Atom::TableItemLeft }, + { "TableItemRight", Atom::TableItemRight }, + { "TableOfContents", Atom::TableOfContents }, + { "Target", Atom::Target }, + { "UnhandledFormat", Atom::UnhandledFormat }, + { "WarningLeft", Atom::WarningLeft }, + { "WarningRight", Atom::WarningRight }, + { "UnknownCommand", Atom::UnknownCommand }, + { nullptr, 0 } }; + +/*! \fn Atom::Atom(AtomType type, const QString &string) + + Constructs an atom of the specified \a type with the single + parameter \a string and does not put the new atom in a list. +*/ + +/*! \fn Atom::Atom(AtomType type, const QString &p1, const QString &p2) + + Constructs an atom of the specified \a type with the two + parameters \a p1 and \a p2 and does not put the new atom + in a list. +*/ + +/*! \fn Atom(Atom *previous, AtomType type, const QString &string) + + Constructs an atom of the specified \a type with the single + parameter \a string and inserts the new atom into the list + after the \a previous atom. +*/ + +/*! \fn Atom::Atom(Atom *previous, AtomType type, const QString &p1, const QString &p2) + + Constructs an atom of the specified \a type with the two + parameters \a p1 and \a p2 and inserts the new atom into + the list after the \a previous atom. +*/ + +/*! \fn void Atom::appendChar(QChar ch) + + Appends \a ch to the string parameter of this atom. + + \also string() +*/ + +/*! \fn void Atom::concatenateString(const QString &string) + + Appends \a string to the string parameter of this atom. + + \also string() +*/ + +/*! \fn void Atom::chopString() + + \also string() +*/ + +/*! + Starting from this Atom, searches the linked list for the + atom of specified type \a t and returns it. Returns \nullptr + if no such atom is found. +*/ +const Atom *Atom::find(AtomType t) const +{ + const auto *a{this}; + while (a && a->type() != t) + a = a->next(); + return a; +} + +/*! + Starting from this Atom, searches the linked list for the + atom of specified type \a t and string \a s, and returns it. + Returns \nullptr if no such atom is found. +*/ +const Atom *Atom::find(AtomType t, const QString &s) const +{ + const auto *a{this}; + while (a && (a->type() != t || a->string() != s)) + a = a->next(); + return a; +} + +/*! \fn Atom *Atom::next() + Return the next atom in the atom list. + \also type(), string() +*/ + +/*! + Return the next Atom in the list if it is of AtomType \a t. + Otherwise return 0. + */ +const Atom *Atom::next(AtomType t) const +{ + return (m_next && (m_next->type() == t)) ? m_next : nullptr; +} + +/*! + Return the next Atom in the list if it is of AtomType \a t + and its string part is \a s. Otherwise return 0. + */ +const Atom *Atom::next(AtomType t, const QString &s) const +{ + return (m_next && (m_next->type() == t) && (m_next->string() == s)) ? m_next : nullptr; +} + +/*! \fn const Atom *Atom::next() const + Return the next atom in the atom list. + \also type(), string() +*/ + +/*! \fn AtomType Atom::type() const + Return the type of this atom. + \also string(), next() +*/ + +/*! + Return the type of this atom as a string. Return "Invalid" if + type() returns an impossible value. + + This is only useful for debugging. + + \also type() +*/ +QString Atom::typeString() const +{ + static bool deja = false; + + if (!deja) { + int i = 0; + while (atms[i].english != nullptr) { + if (atms[i].no != i) + Location::internalError(QStringLiteral("QDoc::Atom: atom %1 missing").arg(i)); + ++i; + } + deja = true; + } + + int i = static_cast<int>(type()); + if (i < 0 || i > static_cast<int>(Last)) + return QLatin1String("Invalid"); + return QLatin1String(atms[i].english); +} + +/*! \fn const QString &Atom::string() const + + Returns the string parameter that together with the type + characterizes this atom. + + \also type(), next() +*/ + +/*! + For a link atom, returns the string representing the link text + if one exist in the list of atoms. +*/ +QString Atom::linkText() const +{ + Q_ASSERT(m_type == Atom::Link); + QString result; + + if (next() && next()->string() == ATOM_FORMATTING_LINK) { + auto *atom = next()->next(); + while (atom && atom->type() != Atom::FormattingRight) { + result += atom->string(); + atom = atom->next(); + } + return result; + } + + return string(); +} + +/*! + The only constructor for LinkAtom. It creates an Atom of + type Atom::Link. \a p1 being the link target. \a p2 is the + parameters in square brackets. Normally there is just one + word in the square brackets, but there can be up to three + words separated by spaces. The constructor splits \a p2 on + the space character. + */ +LinkAtom::LinkAtom(const QString &p1, const QString &p2, Location location) + : Atom(Atom::Link, p1), + location(location), + m_resolved(false), + m_genus(Node::DontCare), + m_domain(nullptr), + m_squareBracketParams(p2) +{ + // nada. +} + +/*! + This function resolves the parameters that were enclosed in + square brackets. If the parameters have already been resolved, + it does nothing and returns immediately. + */ +void LinkAtom::resolveSquareBracketParams() +{ + if (m_resolved) + return; + const QStringList params = m_squareBracketParams.toLower().split(QLatin1Char(' ')); + for (const auto ¶m : params) { + if (!m_domain) { + m_domain = QDocDatabase::qdocDB()->findTree(param); + if (m_domain) { + continue; + } + } + + if (param == "qml") { + m_genus = Node::QML; + continue; + } + if (param == "cpp") { + m_genus = Node::CPP; + continue; + } + if (param == "doc") { + m_genus = Node::DOC; + continue; + } + if (param == "api") { + m_genus = Node::API; + continue; + } + break; + } + m_resolved = true; +} + +/*! + Standard copy constructor of LinkAtom \a t. + */ +LinkAtom::LinkAtom(const LinkAtom &t) + : Atom(Link, t.string()), + location(t.location), + m_resolved(t.m_resolved), + m_genus(t.m_genus), + m_domain(t.m_domain), + m_squareBracketParams(t.m_squareBracketParams) +{ + // nothing +} + +/*! + Special copy constructor of LinkAtom \a t, where + where the new LinkAtom will not be the first one + in the list. + */ +LinkAtom::LinkAtom(Atom *previous, const LinkAtom &t) + : Atom(previous, Link, t.string()), + location(t.location), + m_resolved(t.m_resolved), + m_genus(t.m_genus), + m_domain(t.m_domain), + m_squareBracketParams(t.m_squareBracketParams) +{ + previous->m_next = this; +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/atom.h b/src/qdoc/qdoc/src/qdoc/atom.h new file mode 100644 index 000000000..7483c829e --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/atom.h @@ -0,0 +1,223 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef ATOM_H +#define ATOM_H + +#include "node.h" + +#include <QtCore/qdebug.h> +#include <QtCore/qstringlist.h> + +QT_BEGIN_NAMESPACE + +class Tree; +class LinkAtom; + +class Atom +{ +public: + enum AtomType { + AnnotatedList, + AutoLink, + BaseName, + BR, + BriefLeft, + BriefRight, + C, + CaptionLeft, + CaptionRight, + Code, + CodeBad, + CodeQuoteArgument, + CodeQuoteCommand, + ComparesLeft, + ComparesRight, + DetailsLeft, + DetailsRight, + DivLeft, + DivRight, + ExampleFileLink, + ExampleImageLink, + FootnoteLeft, + FootnoteRight, + FormatElse, + FormatEndif, + FormatIf, + FormattingLeft, + FormattingRight, + GeneratedList, + HR, + Image, + ImageText, + ImportantLeft, + ImportantRight, + InlineImage, + Keyword, + LegaleseLeft, + LegaleseRight, + LineBreak, + Link, + LinkNode, + ListLeft, + ListItemNumber, + ListTagLeft, + ListTagRight, + ListItemLeft, + ListItemRight, + ListRight, + NavAutoLink, + NavLink, + Nop, + NoteLeft, + NoteRight, + ParaLeft, + ParaRight, + Qml, + QuotationLeft, + QuotationRight, + RawString, + SectionLeft, + SectionRight, + SectionHeadingLeft, + SectionHeadingRight, + SidebarLeft, + SidebarRight, + SinceList, + SinceTagLeft, + SinceTagRight, + SnippetCommand, + SnippetIdentifier, + SnippetLocation, + String, + TableLeft, + TableRight, + TableHeaderLeft, + TableHeaderRight, + TableRowLeft, + TableRowRight, + TableItemLeft, + TableItemRight, + TableOfContents, + Target, + UnhandledFormat, + WarningLeft, + WarningRight, + UnknownCommand, + Last = UnknownCommand + }; + + friend class LinkAtom; + + explicit Atom(AtomType type, const QString &string = "") : m_type(type), m_strs(string) { } + + Atom(AtomType type, const QString &p1, const QString &p2) : m_type(type), m_strs(p1) + { + if (!p2.isEmpty()) + m_strs << p2; + } + + Atom(Atom *previous, AtomType type, const QString &string) + : m_next(previous->m_next), m_type(type), m_strs(string) + { + previous->m_next = this; + } + + Atom(Atom *previous, AtomType type, const QString &p1, const QString &p2) + : m_next(previous->m_next), m_type(type), m_strs(p1) + { + if (!p2.isEmpty()) + m_strs << p2; + previous->m_next = this; + } + + virtual ~Atom() = default; + + void appendChar(QChar ch) { m_strs[0] += ch; } + void concatenateString(const QString &string) { m_strs[0] += string; } + void append(const QString &string) { m_strs << string; } + void chopString() { m_strs[0].chop(1); } + void setString(const QString &string) { m_strs[0] = string; } + Atom *next() { return m_next; } + void setNext(Atom *newNext) { m_next = newNext; } + + [[nodiscard]] const Atom *find(AtomType t) const; + [[nodiscard]] const Atom *find(AtomType t, const QString &s) const; + [[nodiscard]] const Atom *next() const { return m_next; } + [[nodiscard]] const Atom *next(AtomType t) const; + [[nodiscard]] const Atom *next(AtomType t, const QString &s) const; + [[nodiscard]] AtomType type() const { return m_type; } + [[nodiscard]] QString typeString() const; + [[nodiscard]] const QString &string() const { return m_strs[0]; } + [[nodiscard]] const QString &string(int i) const { return m_strs[i]; } + [[nodiscard]] qsizetype count() const { return m_strs.size(); } + [[nodiscard]] QString linkText() const; + [[nodiscard]] const QStringList &strings() const { return m_strs; } + + [[nodiscard]] virtual bool isLinkAtom() const { return false; } + virtual Node::Genus genus() { return Node::DontCare; } + virtual Tree *domain() { return nullptr; } + virtual void resolveSquareBracketParams() {} + +protected: + Atom *m_next = nullptr; + AtomType m_type {}; + QStringList m_strs {}; +}; + +class LinkAtom : public Atom +{ +public: + LinkAtom(const QString &p1, const QString &p2, Location location = Location()); + LinkAtom(const LinkAtom &t); + LinkAtom(Atom *previous, const LinkAtom &t); + ~LinkAtom() override = default; + + [[nodiscard]] bool isLinkAtom() const override { return true; } + Node::Genus genus() override + { + resolveSquareBracketParams(); + return m_genus; + } + Tree *domain() override + { + resolveSquareBracketParams(); + return m_domain; + } + void resolveSquareBracketParams() override; + +public: + Location location; + +protected: + bool m_resolved {}; + Node::Genus m_genus {}; + Tree *m_domain {}; + QString m_squareBracketParams {}; +}; + +#define ATOM_FORMATTING_BOLD "bold" +#define ATOM_FORMATTING_INDEX "index" +#define ATOM_FORMATTING_ITALIC "italic" +#define ATOM_FORMATTING_LINK "link" +#define ATOM_FORMATTING_PARAMETER "parameter" +#define ATOM_FORMATTING_SPAN "span " +#define ATOM_FORMATTING_SUBSCRIPT "subscript" +#define ATOM_FORMATTING_SUPERSCRIPT "superscript" +#define ATOM_FORMATTING_TELETYPE "teletype" +#define ATOM_FORMATTING_TRADEMARK "trademark" +#define ATOM_FORMATTING_UICONTROL "uicontrol" +#define ATOM_FORMATTING_UNDERLINE "underline" + +#define ATOM_LIST_BULLET "bullet" +#define ATOM_LIST_TAG "tag" +#define ATOM_LIST_VALUE "value" +#define ATOM_LIST_LOWERALPHA "loweralpha" +#define ATOM_LIST_LOWERROMAN "lowerroman" +#define ATOM_LIST_NUMERIC "numeric" +#define ATOM_LIST_UPPERALPHA "upperalpha" +#define ATOM_LIST_UPPERROMAN "upperroman" + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/boundaries/filesystem/directorypath.cpp b/src/qdoc/qdoc/src/qdoc/boundaries/filesystem/directorypath.cpp new file mode 100644 index 000000000..799582139 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/boundaries/filesystem/directorypath.cpp @@ -0,0 +1,126 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "directorypath.h" + +/*! + * \class DirectoryPath + * + * \brief Represents a path to a directory that was known to exist on the + * filesystem. + * + * An instance of this type guarantees that, at the time of creation + * of the instance, the contained path represented an existing, + * readable, executable directory. + * + * The type is intended to be used whenever a user-provided path to a + * directory is encountered the first time, validating that it can be + * used later on for the duration of a QDoc execution and + * canonicalizing the original path. + * + * Such a usage example could be during the configuration process, + * when encountering the paths that defines where QDoc should search + * for images or other files. + * + * Similarly, it is intended to be used at the API boundaries, + * internally, to relieve the called element of the requirement to + * check the validity of a path when a directory is required and to + * ensure that a single format of the path is encountered. + * + * Do note that the guarantees provided by this type do not + * necessarily hold after the time of creation of an instance. + * Indeed, the underlying filesystem may have changed. + * + * It is possible to renew the contract by obtaining a new instance: + * + * \code + * DirectoryPath old... + * + * ... + * + * auto current{DirectoryPath:refine(old.value())}; + * \endcode + * + * QDoc itself will not generally perform destructive operations on + * its input files during an execution and, as such, it is never + * required to renew a contract. Ensuring that the underlying input + * files are indeed immutable is out-of-scope for QDoc and it is + * allowed to consider a case where the contract was invalidated as + * undefined behavior. + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {wrapped_type_documentation} {DirectoryPath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {has_equality_operator_documentation} {DirectoryPath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {has_less_than_operator_documentation} {DirectoryPath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {has_strictly_less_than_operator_documentation} {DirectoryPath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {has_greater_than_operator_documentation} {DirectoryPath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {has_strictly_greater_than_operator_documentation} {DirectoryPath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {refine_documentation} {DirectoryPath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {value_documentation} {DirectoryPath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {copy_constructor_documentation} {DirectoryPath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {copy_assignment_documentation} {DirectoryPath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {move_constructor_documentation} {DirectoryPath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {move_assignment_documentation} {DirectoryPath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {conversion_documentation} {DirectoryPath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {operator_equal_documentation} {DirectoryPath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {operator_unequal_documentation} {DirectoryPath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {operator_less_than_documentation} {DirectoryPath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {operator_less_than_or_equal_documentation} {DirectoryPath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {operator_greater_than_documentation} {DirectoryPath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {operator_greater_than_or_equal_documentation} {DirectoryPath} + */ diff --git a/src/qdoc/qdoc/src/qdoc/boundaries/filesystem/directorypath.h b/src/qdoc/qdoc/src/qdoc/boundaries/filesystem/directorypath.h new file mode 100644 index 000000000..7ec1dd415 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/boundaries/filesystem/directorypath.h @@ -0,0 +1,17 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "../refined_typedef.h" + +#include <optional> + +#include <QtCore/qstring.h> +#include <QtCore/qfileinfo.h> + +QDOC_REFINED_TYPEDEF(QString, DirectoryPath) { + QFileInfo info{value}; + + return (info.isDir() && info.isReadable() && info.isExecutable()) ? std::optional(DirectoryPath{info.canonicalFilePath()}) : std::nullopt; +} diff --git a/src/qdoc/qdoc/src/qdoc/boundaries/filesystem/filepath.cpp b/src/qdoc/qdoc/src/qdoc/boundaries/filesystem/filepath.cpp new file mode 100644 index 000000000..d8964b6a6 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/boundaries/filesystem/filepath.cpp @@ -0,0 +1,125 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "filepath.h" + +/*! + * \class FilePath + * + * \brief Represents a path to a file that was known to exist on the + * filesystem. + * + * An instance of this type guarantees that, at the time of creation + * of the instance, the contained path represented an existing, + * readable file. + * + * The type is intended to be used whenever a user-provided path to a + * file is encountered the first time, validating that it can be + * used later on for the duration of a QDoc execution and + * canonicalizing the original path. + * + * Such a usage example could be when resolving a file whose path is + * provided by the user. + * + * Similarly, it is intended to be used at the API boundaries, + * internally, to relieve the called element of the requirement to + * check the validity of a path when a file is required and to + * ensure that a single format of the path is encountered. + * + * Do note that the guarantees provided by this type do not + * necessarily hold after the time of creation of an instance. + * Indeed, the underlying filesystem may have changed. + * + * It is possible to renew the contract by obtaining a new instance: + * + * \code + * FilePath old... + * + * ... + * + * auto current{FilePath::refine(old.value())}; + * \endcode + * + * QDoc itself will not generally perform destructive operations on + * its input files during an execution and, as such, it is never + * required to renew a contract. Ensuring that the underlying input + * files are indeed immutable is out-of-scope for QDoc and it is + * allowed to consider a case where the contract was invalidated as + * undefined behavior. + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {wrapped_type_documentation} {FilePath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {has_equality_operator_documentation} {FilePath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {has_less_than_operator_documentation} {FilePath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {has_strictly_less_than_operator_documentation} {FilePath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {has_greater_than_operator_documentation} {FilePath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {has_strictly_greater_than_operator_documentation} {FilePath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {refine_documentation} {FilePath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {value_documentation} {FilePath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {copy_constructor_documentation} {FilePath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {copy_assignment_documentation} {FilePath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {move_constructor_documentation} {FilePath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {move_assignment_documentation} {FilePath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {conversion_documentation} {FilePath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {operator_equal_documentation} {FilePath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {operator_unequal_documentation} {FilePath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {operator_less_than_documentation} {FilePath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {operator_less_than_or_equal_documentation} {FilePath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {operator_greater_than_documentation} {FilePath} + */ + +/*! + * \include boundaries/refined_typedef_members.qdocinc {operator_greater_than_or_equal_documentation} {FilePath} + */ diff --git a/src/qdoc/qdoc/src/qdoc/boundaries/filesystem/filepath.h b/src/qdoc/qdoc/src/qdoc/boundaries/filesystem/filepath.h new file mode 100644 index 000000000..e8a9b2d35 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/boundaries/filesystem/filepath.h @@ -0,0 +1,17 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "qdoc/boundaries/refined_typedef.h" + +#include <optional> + +#include <QtCore/qstring.h> +#include <QtCore/qfileinfo.h> + +QDOC_REFINED_TYPEDEF(QString, FilePath) { + QFileInfo info{value}; + + return (info.isFile() && info.isReadable()) ? std::optional(FilePath{info.canonicalFilePath()}) : std::nullopt; +} diff --git a/src/qdoc/qdoc/src/qdoc/boundaries/filesystem/resolvedfile.cpp b/src/qdoc/qdoc/src/qdoc/boundaries/filesystem/resolvedfile.cpp new file mode 100644 index 000000000..4d5eca512 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/boundaries/filesystem/resolvedfile.cpp @@ -0,0 +1,96 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "resolvedfile.h" + +/*! + * \class ResolvedFile + * + * \brief Represents a file that is reachable by QDoc based on its + * current configuration. + * + * Instances of this type are, generally, intended to be generated by + * any process that needs to query the filesystem for the presence of + * some files based on a user-inputted path to ensure their + * availability. + * + * Such an example might be when QDoc is searching for a file whose + * path is provided by the user, such as the one in a snippet command, + * that should represent a file that is reachable with the current + * configuration. + * + * On the other side, logic that requires access to files that are + * known to be user-provided, such as the quoting of snippets, can use + * this type at the API boundary to signal that the file should be + * accessible so that they avoid the need to search for the file + * themselves. + * + * Do note that, semantically, this type doesn't actually guarantee + * anything about its origin and only guarantees whatever its members + * guarantee. + * + * The reasoning behind this lack of enforcement is to allow for an + * easier testing. + * As many parts of QDoc might require the presence of an instance of + * this type, we want to be able to construct those instances without + * the need to pass trough whichever valid generator for them. + * + * Nonetheless, inside QDoc, any boundary that requires an instance of + * this type can consider it guaranteed that the instance was + * generated trough some appropriate logic, and consider it a bug if + * such is not the case. + * + * An instance of this type provides two pieces of information. + * + * The path to the file that is considered resolved, accessible trough + * the get_path() method and the string that was used to resolve the + * file in the first place, accessible trough the get_query() method. + * + * The first should be used by consumer who needs to interact with the + * file itself, such as reading from it or copying it. + * + * The second is provided for context and can be used when consumers + * need to know what the user-inputted path was in the first place, + * for example when presenting debug information. + * + * It is not semantically guaranteed that this two pieces of + * information are actually related. Any such instance for which this + * is true should be considered malformed. Inside QDoc, tough, + * consumer of this type can consider it guaranteed that no malformed + * instance will be passed to them, and consider it a bug if it + * happens otherwise. + */ + +/*! + * \fn ResolvedFile::ResolvedFile(QString query, FilePath filepath) + * + * Constructs an instance of this type from \a query and \a filepath. + * + * \a query should represent the user-inputted path that was used to + * resolve the file that this instance represents. + * + * \a filepath should represent the file that is found querying the + * filesystem trough \a query using an appropriate logic for resolving + * files. + * + * An instance that is built from \a query and \a filepath is + * guaranteed to return a value that is equivalent to \a query when + * get_query() is called and a value that is equivalent to \a + * filepath.value() when get_path() is called. + */ + +/*! + * \fn const QString& ResolvedFile::get_query() const + * + * Returns a string representing the user-inputted path that was used + * to resolve the file. + */ + +/*! + * \fn const QString& ResolvedFile::get_path() const + * + * Returns a string representing the canonicalized path to the file + * that was resolved. + * + * Access to this file is to be considered guranteed to be available. + */ diff --git a/src/qdoc/qdoc/src/qdoc/boundaries/filesystem/resolvedfile.h b/src/qdoc/qdoc/src/qdoc/boundaries/filesystem/resolvedfile.h new file mode 100644 index 000000000..e21120ab1 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/boundaries/filesystem/resolvedfile.h @@ -0,0 +1,20 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "qdoc/boundaries/filesystem/filepath.h" + +#include <QString> + +struct ResolvedFile { +public: + ResolvedFile(QString query, FilePath filepath) : query{query}, filepath{filepath} {} + + [[nodiscard]] const QString& get_query() const { return query; } + [[nodiscard]] const QString& get_path() const { return filepath.value(); } + +private: + QString query; + FilePath filepath; +}; diff --git a/src/qdoc/qdoc/src/qdoc/boundaries/refined_typedef.h b/src/qdoc/qdoc/src/qdoc/boundaries/refined_typedef.h new file mode 100644 index 000000000..4f3a13b7c --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/boundaries/refined_typedef.h @@ -0,0 +1,207 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include <QtCore/qglobal.h> + +#include <functional> +#include <optional> +#include <type_traits> + +// TODO: Express the documentation such that QDoc would be able to see +// it and process it correctly. This probably means that we would like +// to associate the definition with a namespace, albeit we could use +// the header file too, and put the documentation in an empty cpp +// file. This is delayed as there currently isn't much namespacing for +// anything in QDoc and such a namespacing should be added gradually +// and attentively. + +// TODO: Review the semantics for construction and optmize it. Should we copy the +// value? Should we only allow rvalues? + +// TODO: There is an high chance that we will need to "compose" +// refinitions later on when dealing, for example, with the basics of +// user-provided paths. +// For example, when requiring that each user-inputted path is purged +// as per QFileInfo definition of purging. +// For example, it might be that instead of passing QString around we +// might pass some Path type that is a purged QString. +// Then, any other refinement over paths will need to use that as a +// base type. +// To avoid the clutter that comes from that, if such will be the +// case, we will need to change the definition of refine and value if +// the passed in type was refined already. +// That is, such that if we have: +// +// QDOC_REFINE_TYPE(QString, Path) { ... } +// QDOC_REFINE_TYPE(Path, Foo) { ... } +// +// Foo refines a QString and Foo.value returns a QString. This should +// in general be trivial as long as we add a way to identify, such as +// a tag that refinements derive from, what type was declared through +// QDOC_REFINED_TYPEDEF and what type was not. + +// TODO: Provide a way to generate a standard documentation for all +// members of a type generated by QDOC_REFINED_TYPEDEF without having +// to copy-paste include command everywhere. +// The main problem of doing this is that the preprocessor strips +// comments away, making it impossible to generate comments, and hence +// QDoc documentation, with the preprocessor. + +/*! + * \macro QDOC_REFINED_TYPEDEF(_type, _name) + * \relates refined_typedef.hpp + * + * Declares a wrapper type for \c {_type}, with identifier \c {_name}, + * that represents a subset of \c {_type} for which some conditions + * hold. + * + * For example: + * + * \code + QDOC_REFINED_TYPEDEF(std::size_t, Fin5) { + return (value < 5) : std::make_optional<Fin5>{value} : std::nullopt; + } + * \endcode + * + * Represents the subset of \c {std::size_t} that contains the value 0, 1, + * 2, 3 and 4, that is, the general finite set of cardinality 5. + * + * As the example shows, usages of the macro require some type, an + * identifier and some code. + * + * The type that is provided is the type that will be wrapped. + * Do note that we expect a type with no-qualifiers and that is not a + * pointer type. Types passed with those kind of qualifiers will be + * simplified to their base type. + * + * That is, for example, \c {int*}, \c {const int}, \c {const int&}, + * \c {int&} all counts as \c {int}. + * + * The identifier that is passed is used as the name for the newly + * declared type that wraps the original type. + * + * The code block that is passed will be run when an instance of the + * newly created wrapper type is being obtained. + * If the wrapper type is T, the codeblock must return a \c + * {std::optional<T>}. + * The code block should perform any check that ensures that the + * guarantees provided by the wrapper type holds and return a value if + * such is the case, otherwise returning \c {std::nullopt}. + * + * Inside the code block, the identifier \e {value} is implicitly + * bound to an element of the wrapped type that the instance is being + * built from and for which the guarantees provided by the wrapper + * type must hold. + * + * When a call to QDOC_REFINED_TYPEDEF is successful, a type with the + * provided identifier is declared. + * + * Let T be a type declared trough a call of QDOC_REFINED_TYPEDEF and + * W be the type that it wraps. + * + * An instance of T can be obtained by calling T::refine with an + * element of W. + * + * If the element of W respects the guarantees that T provides, then + * the call will return an optional that contains an instance of T, + * othewise it will return an empty optional. + * + * When an instance of T is obtained, it will wrap the element of W that + * was used to obtain it. + * + * The wrapped value can be accessed trough the \c {value} method. + * + * For example, considering \c {Fin5}, we could obtain an instance of + * it as follows: + * + * \code + * auto instance = *(Fin5::refine(std::size_t{1})); + * \endcode + * + * With that instance available we can retrieve the original value as + * follows: + * + * \code + * instance.value(); // The value 1 + * \endcode + */ + +#define QDOC_REFINED_TYPEDEF(_type, _name) \ + struct _name { \ + public: \ + using wrapped_type = std::remove_reference_t<std::remove_cv_t<std::remove_pointer_t<_type>>>; \ + \ + inline static constexpr auto has_equality_operator_v = std::is_invocable_r_v<bool, std::equal_to<>, wrapped_type, wrapped_type>; \ + inline static constexpr auto has_less_than_operator_v = std::is_invocable_r_v<bool, std::less_equal<>, wrapped_type, wrapped_type>; \ + inline static constexpr auto has_strictly_less_than_operator_v = std::is_invocable_r_v<bool, std::less<>, wrapped_type, wrapped_type>; \ + inline static constexpr auto has_greater_than_operator_v = std::is_invocable_r_v<bool, std::greater_equal<>, wrapped_type, wrapped_type>; \ + inline static constexpr auto has_strictly_greater_than_operator_v = std::is_invocable_r_v<bool, std::greater<>, wrapped_type, wrapped_type>; \ + \ + public: \ + static std::optional<_name> refine(wrapped_type value); \ + \ + [[nodiscard]] const wrapped_type& value() const noexcept { return _value; } \ + \ + _name(const _name&) = default; \ + _name& operator=(const _name&) = default; \ + \ + _name(_name&&) = default; \ + _name& operator=(_name&&) = default; \ + \ + operator wrapped_type() const { return _value; } \ + \ + public: \ + \ + template< \ + typename = std::enable_if_t< \ + has_equality_operator_v \ + > \ + > \ + bool operator==(const _name& rhs) const noexcept { return _value == rhs._value; } \ + \ + template< \ + typename = std::enable_if_t< \ + has_equality_operator_v \ + > \ + > \ + bool operator!=(const _name& rhs) const noexcept { return !(_value == rhs._value); } \ + \ + template< \ + typename = std::enable_if_t< \ + has_less_than_operator_v \ + > \ + > \ + bool operator<=(const _name& rhs) const noexcept { return _value <= rhs._value; } \ + \ + \ + template< \ + typename = std::enable_if_t< \ + has_strictly_less_than_operator_v \ + > \ + > \ + bool operator<(const _name& rhs) const noexcept { return _value < rhs._value; } \ + \ + template< \ + typename = std::enable_if_t< \ + has_greater_than_operator_v \ + > \ + > \ + bool operator>=(const _name& rhs) const noexcept { return _value >= rhs._value; } \ + \ + template< \ + typename = std::enable_if_t< \ + has_strictly_greater_than_operator_v \ + > \ + > \ + bool operator>(const _name& rhs) const noexcept { return _value > rhs._value; } \ + \ + private: \ + _name(wrapped_type value) : _value{std::move(value)} {} \ + \ + private: \ + wrapped_type _value; \ + }; \ + \ + inline std::optional<_name> _name::refine(wrapped_type value) diff --git a/src/qdoc/qdoc/src/qdoc/boundaries/refined_typedef_members.qdocinc b/src/qdoc/qdoc/src/qdoc/boundaries/refined_typedef_members.qdocinc new file mode 100644 index 000000000..ab956f49f --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/boundaries/refined_typedef_members.qdocinc @@ -0,0 +1,162 @@ +//! [wrapped_type_documentation] +\typealias \1::wrapped_type + +The type that is wrapped by this type. + +This type is always deprived of qualifiers and is never a pointer +type. +//! [wrapped_type_documentation] + +//! [has_equality_operator_documentation] +\variable \1::has_equality_operator_v + +True when the wrapped_type can be compared for equality. + +When this is the case, \1 can be compared for equality and inequality. +//! [has_equality_operator_documentation] + +//! [has_less_than_operator_documentation] +\variable \1::has_less_than_operator_v + +True when the wrapped_type can be compared for lesserness. + +When this is the case, \1 can be compared for lesserness. +//! [has_less_than_operator_documentation] + +//! [has_strictly_less_than_operator_documentation] +\variable \1::has_strictly_less_than_operator_v + +True when the wrapped_type can be compared for strict lesserness. + +When this is the case, \1 can be compared for strict lesserness. +//! [has_stricly_less_than_operator_documentation] + +//! [has_greater_than_operator_documentation] +\variable \1::has_greater_than_operator_v + +True when the wrapped_type can be compared for greaterness. + +When this is the case, \1 can be compared for greaterness. +//! [has_less_than_operator_documentation] + +//! [has_strictly_greater_than_operator_documentation] +\variable \1::has_strictly_greater_than_operator_v + +True when the wrapped_type can be compared for strict greaterness. + +When this is the case, \1 can be compared for strict greaterness. +//! [has_stricly_greater_than_operator_documentation] + +//! [refine_documentation] +\fn static std::optional<\1> \1::refine(wrapped_type value) + +Returns an instance of \1 wrapping \a value if \a value respects the +guarantees that are required by \1. + +If such is not the case, \c {std::nullopt} is returned instead. +//! [refine_documentation] + +//! [value_documentation] +\fn const wrapped_type& \1::value() const noexcept + +Returns a const reference to the value that is wrapped by this +instance. +//! [value_documentation] + +//! [copy_constructor_documentation] +\fn \1::\1(const \1& other) + +Copy-constructs an instance of \1 from \a other. + +This constructor is generated by the compiler. +//! [copy_constructor_documentation] + +//! [copy_assignment_documentation] +\fn \1::operator=(const \1& other) + +Copy-assigns to this instance of \1 from \a other. + +This constructor is generated by the compiler. +//! [copy_assignment_documentation] + +//! [move_constructor_documentation] +\fn \1::\1(\1&& other) + +Move-constructs an instance of \1 from \a other. + +The only valid operations on an instance that was moved-from are +destruction and reassignment. + +This constructor is generated by the compiler. +//! [move_constructor_documentation] + +//! [move_assignment_documentation] +\fn \1::operator=(\1&& other) + +Move-assigns to this instance of \1 from \a other. + +The only valid operations on an instance that was moved-from are +destruction and reassignment. + +This constructor is generated by the compiler. +//! [move_assignment_documentation] + +//! [conversion_documentation] +\fn operator wrapped_type() const + +Converts this instance to its wrapped value. +//! [conversion_documentation] + +//! [operator_equal_documentation] +\fn bool operator=(const \1& rhs) const noexcept + +Returns true if the value wrapped by this instance and \a rhs compare +equal. + +Returns false otherwise. +//! [operator_equal_documentation] + +//! [operator_unequal_documentation] +\fn bool operator!=(const \1& rhs) const noexcept + +Returns true if the value wrapped by this instance and \a rhs do not +compare equal. + +Returns false otherwise. +//! [operator_unequal_documentation] + +//! [operator_less_than_documentation] +\fn bool operator<(const \1& rhs) const noexcept + +Returns true if the value wrapped by this instance compares less than +the value wrapped by \a rhs. + +Returns false otherwise. +//! [operator_less_than_documentation] + +//! [operator_less_than_or_equal_documentation] +\fn bool operator<=(const \1& rhs) const noexcept + +Returns true if the value wrapped by this instance compares less than +or equal than the value wrapped by \a rhs. + +Returns false otherwise. +//! [operator_less_than_or_equal_documentation] + +//! [operator_greater_than_documentation] +\fn bool operator>(const \1& rhs) const noexcept + +Returns true if the value wrapped by this instance compares greater +than the value wrapped by \a rhs. + +Returns false otherwise. +//! [operator_greater_than_documentation] + +//! [operator_greater_than_or_equal_documentation] +\fn bool operator>=(const \1& rhs) const noexcept + +Returns true if the value wrapped by this instance compares greater +than or equal or equal than the value wrapped by \a rhs. + +Returns false otherwise. +//! [operator_greater_than_or_equal_documentation] diff --git a/src/qdoc/qdoc/src/qdoc/clang/AST/LLVM_LICENSE.txt b/src/qdoc/qdoc/src/qdoc/clang/AST/LLVM_LICENSE.txt new file mode 100644 index 000000000..fa6ac5400 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/clang/AST/LLVM_LICENSE.txt @@ -0,0 +1,279 @@ +============================================================================== +The LLVM Project is under the Apache License v2.0 with LLVM Exceptions: +============================================================================== + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + +============================================================================== +Software from third parties included in the LLVM Project: +============================================================================== +The LLVM Project contains third party software which is under different license +terms. All such code will be identified clearly using at least one of two +mechanisms: +1) It will be in a separate directory tree with its own `LICENSE.txt` or + `LICENSE` file at the top containing the specific license and restrictions + which apply to that software, or +2) It will contain specific license and restriction terms at the top of every + file. + +============================================================================== +Legacy LLVM License (https://llvm.org/docs/DeveloperPolicy.html#legacy): +============================================================================== +University of Illinois/NCSA +Open Source License + +Copyright (c) 2003-2019 University of Illinois at Urbana-Champaign. +All rights reserved. + +Developed by: + + LLVM Team + + University of Illinois at Urbana-Champaign + + http://llvm.org + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal with +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimers. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimers in the + documentation and/or other materials provided with the distribution. + + * Neither the names of the LLVM Team, University of Illinois at + Urbana-Champaign, nor the names of its contributors may be used to + endorse or promote products derived from this Software without specific + prior written permission. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE +SOFTWARE. + diff --git a/src/qdoc/qdoc/src/qdoc/clang/AST/QualTypeNames.h b/src/qdoc/qdoc/src/qdoc/clang/AST/QualTypeNames.h new file mode 100644 index 000000000..c6d331ea8 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/clang/AST/QualTypeNames.h @@ -0,0 +1,491 @@ +//===------- QualTypeNames.cpp - Generate Complete QualType Names ---------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#pragma once + +// Those directives indirectly includes "clang/AST/Attrs.h" which +// includes "clang/AST/Attrs.inc". +// "clang/AST/Attrs.inc", produces some "C4267" warnings specifically +// on MSVC 2019. +// This in turn blocks CI integrations for configuration with that +// compiler that treats warnings as errors. +// As that header is not under our control, we disable the warning +// completely when going through those includes. +#include <QtCore/qcompilerdetection.h> + +QT_WARNING_PUSH +QT_WARNING_DISABLE_MSVC(4267) + +#include "clang/AST/DeclTemplate.h" +#include "clang/AST/DeclarationName.h" +#include "clang/AST/GlobalDecl.h" +#include "clang/AST/Mangle.h" + +QT_WARNING_POP + +#include <stdio.h> +#include <memory> + +namespace clang { + +namespace TypeName { + +inline QualType getFullyQualifiedType(QualType QT, const ASTContext &Ctx, + bool WithGlobalNsPrefix); + +/// Create a NestedNameSpecifier for Namesp and its enclosing +/// scopes. +/// +/// \param[in] Ctx - the AST Context to be used. +/// \param[in] Namesp - the NamespaceDecl for which a NestedNameSpecifier +/// is requested. +/// \param[in] WithGlobalNsPrefix - Indicate whether the global namespace +/// specifier "::" should be prepended or not. +static inline NestedNameSpecifier *createNestedNameSpecifier( + const ASTContext &Ctx, + const NamespaceDecl *Namesp, + bool WithGlobalNsPrefix); + +/// Create a NestedNameSpecifier for TagDecl and its enclosing +/// scopes. +/// +/// \param[in] Ctx - the AST Context to be used. +/// \param[in] TD - the TagDecl for which a NestedNameSpecifier is +/// requested. +/// \param[in] FullyQualify - Convert all template arguments into fully +/// qualified names. +/// \param[in] WithGlobalNsPrefix - Indicate whether the global namespace +/// specifier "::" should be prepended or not. +static inline NestedNameSpecifier *createNestedNameSpecifier( + const ASTContext &Ctx, const TypeDecl *TD, + bool FullyQualify, bool WithGlobalNsPrefix); + +static inline NestedNameSpecifier *createNestedNameSpecifierForScopeOf( + const ASTContext &Ctx, const Decl *decl, + bool FullyQualified, bool WithGlobalNsPrefix); + +static inline NestedNameSpecifier *getFullyQualifiedNestedNameSpecifier( + const ASTContext &Ctx, NestedNameSpecifier *scope, bool WithGlobalNsPrefix); + +static inline bool getFullyQualifiedTemplateName(const ASTContext &Ctx, + TemplateName &TName, + bool WithGlobalNsPrefix) { + bool Changed = false; + NestedNameSpecifier *NNS = nullptr; + + TemplateDecl *ArgTDecl = TName.getAsTemplateDecl(); + // ArgTDecl won't be NULL because we asserted that this isn't a + // dependent context very early in the call chain. + assert(ArgTDecl != nullptr); + QualifiedTemplateName *QTName = TName.getAsQualifiedTemplateName(); + + if (QTName && !QTName->hasTemplateKeyword()) { + NNS = QTName->getQualifier(); + NestedNameSpecifier *QNNS = getFullyQualifiedNestedNameSpecifier( + Ctx, NNS, WithGlobalNsPrefix); + if (QNNS != NNS) { + Changed = true; + NNS = QNNS; + } else { + NNS = nullptr; + } + } else { + NNS = createNestedNameSpecifierForScopeOf( + Ctx, ArgTDecl, true, WithGlobalNsPrefix); + } + if (NNS) { + TemplateName UnderlyingTN(ArgTDecl); + if (UsingShadowDecl *USD = TName.getAsUsingShadowDecl()) + UnderlyingTN = TemplateName(USD); + TName = + Ctx.getQualifiedTemplateName(NNS, + /*TemplateKeyword=*/false, UnderlyingTN); + Changed = true; + } + return Changed; +} + +static inline bool getFullyQualifiedTemplateArgument(const ASTContext &Ctx, + TemplateArgument &Arg, + bool WithGlobalNsPrefix) { + bool Changed = false; + + // Note: we do not handle TemplateArgument::Expression, to replace it + // we need the information for the template instance decl. + + if (Arg.getKind() == TemplateArgument::Template) { + TemplateName TName = Arg.getAsTemplate(); + Changed = getFullyQualifiedTemplateName(Ctx, TName, WithGlobalNsPrefix); + if (Changed) { + Arg = TemplateArgument(TName); + } + } else if (Arg.getKind() == TemplateArgument::Type) { + QualType SubTy = Arg.getAsType(); + // Check if the type needs more desugaring and recurse. + QualType QTFQ = getFullyQualifiedType(SubTy, Ctx, WithGlobalNsPrefix); + if (QTFQ != SubTy) { + Arg = TemplateArgument(QTFQ); + Changed = true; + } + } + return Changed; +} + +static inline const Type *getFullyQualifiedTemplateType(const ASTContext &Ctx, + const Type *TypePtr, + bool WithGlobalNsPrefix) { + // DependentTemplateTypes exist within template declarations and + // definitions. Therefore we shouldn't encounter them at the end of + // a translation unit. If we do, the caller has made an error. + assert(!isa<DependentTemplateSpecializationType>(TypePtr)); + // In case of template specializations, iterate over the arguments + // and fully qualify them as well. + if (const auto *TST = dyn_cast<const TemplateSpecializationType>(TypePtr)) { + bool MightHaveChanged = false; + SmallVector<TemplateArgument, 4> FQArgs; + // Cheap to copy and potentially modified by + // getFullyQualifedTemplateArgument. + for (TemplateArgument Arg : TST->template_arguments()) { + MightHaveChanged |= getFullyQualifiedTemplateArgument( + Ctx, Arg, WithGlobalNsPrefix); + FQArgs.push_back(Arg); + } + + // If a fully qualified arg is different from the unqualified arg, + // allocate new type in the AST. + if (MightHaveChanged) { + QualType QT = Ctx.getTemplateSpecializationType( + TST->getTemplateName(), FQArgs, + TST->getCanonicalTypeInternal()); + // getTemplateSpecializationType returns a fully qualified + // version of the specialization itself, so no need to qualify + // it. + return QT.getTypePtr(); + } + } else if (const auto *TSTRecord = dyn_cast<const RecordType>(TypePtr)) { + // We are asked to fully qualify and we have a Record Type, + // which can point to a template instantiation with no sugar in any of + // its template argument, however we still need to fully qualify them. + + if (const auto *TSTDecl = + dyn_cast<ClassTemplateSpecializationDecl>(TSTRecord->getDecl())) { + const TemplateArgumentList &TemplateArgs = TSTDecl->getTemplateArgs(); + + bool MightHaveChanged = false; + SmallVector<TemplateArgument, 4> FQArgs; + for (unsigned int I = 0, E = TemplateArgs.size(); I != E; ++I) { + // cheap to copy and potentially modified by + // getFullyQualifedTemplateArgument + TemplateArgument Arg(TemplateArgs[I]); + MightHaveChanged |= getFullyQualifiedTemplateArgument( + Ctx, Arg, WithGlobalNsPrefix); + FQArgs.push_back(Arg); + } + + // If a fully qualified arg is different from the unqualified arg, + // allocate new type in the AST. + if (MightHaveChanged) { + TemplateName TN(TSTDecl->getSpecializedTemplate()); + QualType QT = Ctx.getTemplateSpecializationType( + TN, FQArgs, + TSTRecord->getCanonicalTypeInternal()); + // getTemplateSpecializationType returns a fully qualified + // version of the specialization itself, so no need to qualify + // it. + return QT.getTypePtr(); + } + } + } + return TypePtr; +} + +static inline NestedNameSpecifier *createOuterNNS(const ASTContext &Ctx, const Decl *D, + bool FullyQualify, + bool WithGlobalNsPrefix) { + const DeclContext *DC = D->getDeclContext(); + if (const auto *NS = dyn_cast<NamespaceDecl>(DC)) { + while (NS && NS->isInline()) { + // Ignore inline namespace; + NS = dyn_cast<NamespaceDecl>(NS->getDeclContext()); + } + if (NS && NS->getDeclName()) { + return createNestedNameSpecifier(Ctx, NS, WithGlobalNsPrefix); + } + return nullptr; // no starting '::', no anonymous + } else if (const auto *TD = dyn_cast<TagDecl>(DC)) { + return createNestedNameSpecifier(Ctx, TD, FullyQualify, WithGlobalNsPrefix); + } else if (const auto *TDD = dyn_cast<TypedefNameDecl>(DC)) { + return createNestedNameSpecifier( + Ctx, TDD, FullyQualify, WithGlobalNsPrefix); + } else if (WithGlobalNsPrefix && DC->isTranslationUnit()) { + return NestedNameSpecifier::GlobalSpecifier(Ctx); + } + return nullptr; // no starting '::' if |WithGlobalNsPrefix| is false +} + +/// Return a fully qualified version of this name specifier. +static inline NestedNameSpecifier *getFullyQualifiedNestedNameSpecifier( + const ASTContext &Ctx, NestedNameSpecifier *Scope, + bool WithGlobalNsPrefix) { + switch (Scope->getKind()) { + case NestedNameSpecifier::Global: + // Already fully qualified + return Scope; + case NestedNameSpecifier::Namespace: + return TypeName::createNestedNameSpecifier( + Ctx, Scope->getAsNamespace(), WithGlobalNsPrefix); + case NestedNameSpecifier::NamespaceAlias: + // Namespace aliases are only valid for the duration of the + // scope where they were introduced, and therefore are often + // invalid at the end of the TU. So use the namespace name more + // likely to be valid at the end of the TU. + return TypeName::createNestedNameSpecifier( + Ctx, + Scope->getAsNamespaceAlias()->getNamespace()->getCanonicalDecl(), + WithGlobalNsPrefix); + case NestedNameSpecifier::Identifier: + // A function or some other construct that makes it un-namable + // at the end of the TU. Skip the current component of the name, + // but use the name of it's prefix. + return getFullyQualifiedNestedNameSpecifier( + Ctx, Scope->getPrefix(), WithGlobalNsPrefix); + case NestedNameSpecifier::Super: + case NestedNameSpecifier::TypeSpec: + case NestedNameSpecifier::TypeSpecWithTemplate: { + const Type *Type = Scope->getAsType(); + // Find decl context. + const TagDecl *TD = nullptr; + if (const TagType *TagDeclType = Type->getAs<TagType>()) { + TD = TagDeclType->getDecl(); + } else { + TD = Type->getAsCXXRecordDecl(); + } + if (TD) { + return TypeName::createNestedNameSpecifier(Ctx, TD, + true /*FullyQualified*/, + WithGlobalNsPrefix); + } else if (const auto *TDD = dyn_cast<TypedefType>(Type)) { + return TypeName::createNestedNameSpecifier(Ctx, TDD->getDecl(), + true /*FullyQualified*/, + WithGlobalNsPrefix); + } + return Scope; + } + } + llvm_unreachable("bad NNS kind"); +} + +/// Create a nested name specifier for the declaring context of +/// the type. +static inline NestedNameSpecifier *createNestedNameSpecifierForScopeOf( + const ASTContext &Ctx, const Decl *Decl, + bool FullyQualified, bool WithGlobalNsPrefix) { + assert(Decl); + + const DeclContext *DC = Decl->getDeclContext()->getRedeclContext(); + const auto *Outer = dyn_cast_or_null<NamedDecl>(DC); + const auto *OuterNS = dyn_cast_or_null<NamespaceDecl>(DC); + if (Outer && !(OuterNS && OuterNS->isAnonymousNamespace())) { + if (OuterNS) { + return createNestedNameSpecifier(Ctx, OuterNS, WithGlobalNsPrefix); + } else if (const auto *TD = dyn_cast<TagDecl>(Outer)) { + return createNestedNameSpecifier( + Ctx, TD, FullyQualified, WithGlobalNsPrefix); + } else if (isa<TranslationUnitDecl>(Outer)) { + // Context is the TU. Nothing needs to be done. + return nullptr; + } else { + // Decl's context was neither the TU, a namespace, nor a + // TagDecl, which means it is a type local to a scope, and not + // accessible at the end of the TU. + return nullptr; + } + } else if (WithGlobalNsPrefix && DC->isTranslationUnit()) { + return NestedNameSpecifier::GlobalSpecifier(Ctx); + } + return nullptr; +} + +/// Create a nested name specifier for the declaring context of +/// the type. +static inline NestedNameSpecifier *createNestedNameSpecifierForScopeOf( + const ASTContext &Ctx, const Type *TypePtr, + bool FullyQualified, bool WithGlobalNsPrefix) { + if (!TypePtr) return nullptr; + + Decl *Decl = nullptr; + // There are probably other cases ... + if (const auto *TDT = dyn_cast<TypedefType>(TypePtr)) { + Decl = TDT->getDecl(); + } else if (const auto *TagDeclType = dyn_cast<TagType>(TypePtr)) { + Decl = TagDeclType->getDecl(); + } else if (const auto *TST = dyn_cast<TemplateSpecializationType>(TypePtr)) { + Decl = TST->getTemplateName().getAsTemplateDecl(); + } else { + Decl = TypePtr->getAsCXXRecordDecl(); + } + + if (!Decl) return nullptr; + + return createNestedNameSpecifierForScopeOf( + Ctx, Decl, FullyQualified, WithGlobalNsPrefix); +} + +inline NestedNameSpecifier *createNestedNameSpecifier(const ASTContext &Ctx, + const NamespaceDecl *Namespace, + bool WithGlobalNsPrefix) { + while (Namespace && Namespace->isInline()) { + // Ignore inline namespace; + Namespace = dyn_cast<NamespaceDecl>(Namespace->getDeclContext()); + } + if (!Namespace) return nullptr; + + bool FullyQualified = true; // doesn't matter, DeclContexts are namespaces + return NestedNameSpecifier::Create( + Ctx, + createOuterNNS(Ctx, Namespace, FullyQualified, WithGlobalNsPrefix), + Namespace); +} + +inline NestedNameSpecifier *createNestedNameSpecifier(const ASTContext &Ctx, + const TypeDecl *TD, + bool FullyQualify, + bool WithGlobalNsPrefix) { + const Type *TypePtr = TD->getTypeForDecl(); + if (isa<const TemplateSpecializationType>(TypePtr) || + isa<const RecordType>(TypePtr)) { + // We are asked to fully qualify and we have a Record Type (which + // may point to a template specialization) or Template + // Specialization Type. We need to fully qualify their arguments. + + TypePtr = getFullyQualifiedTemplateType(Ctx, TypePtr, WithGlobalNsPrefix); + } + + return NestedNameSpecifier::Create( + Ctx, createOuterNNS(Ctx, TD, FullyQualify, WithGlobalNsPrefix), + false /*No TemplateKeyword*/, TypePtr); +} + +/// Return the fully qualified type, including fully-qualified +/// versions of any template parameters. +inline QualType getFullyQualifiedType(QualType QT, const ASTContext &Ctx, + bool WithGlobalNsPrefix = false) { + // In case of myType* we need to strip the pointer first, fully + // qualify and attach the pointer once again. + if (isa<PointerType>(QT.getTypePtr())) { + // Get the qualifiers. + Qualifiers Quals = QT.getQualifiers(); + QT = getFullyQualifiedType(QT->getPointeeType(), Ctx, WithGlobalNsPrefix); + QT = Ctx.getPointerType(QT); + // Add back the qualifiers. + QT = Ctx.getQualifiedType(QT, Quals); + return QT; + } + + if (auto *MPT = dyn_cast<MemberPointerType>(QT.getTypePtr())) { + // Get the qualifiers. + Qualifiers Quals = QT.getQualifiers(); + // Fully qualify the pointee and class types. + QT = getFullyQualifiedType(QT->getPointeeType(), Ctx, WithGlobalNsPrefix); + QualType Class = getFullyQualifiedType(QualType(MPT->getClass(), 0), Ctx, + WithGlobalNsPrefix); + QT = Ctx.getMemberPointerType(QT, Class.getTypePtr()); + // Add back the qualifiers. + QT = Ctx.getQualifiedType(QT, Quals); + return QT; + } + + // In case of myType& we need to strip the reference first, fully + // qualify and attach the reference once again. + if (isa<ReferenceType>(QT.getTypePtr())) { + // Get the qualifiers. + bool IsLValueRefTy = isa<LValueReferenceType>(QT.getTypePtr()); + Qualifiers Quals = QT.getQualifiers(); + QT = getFullyQualifiedType(QT->getPointeeType(), Ctx, WithGlobalNsPrefix); + // Add the r- or l-value reference type back to the fully + // qualified one. + if (IsLValueRefTy) + QT = Ctx.getLValueReferenceType(QT); + else + QT = Ctx.getRValueReferenceType(QT); + // Add back the qualifiers. + QT = Ctx.getQualifiedType(QT, Quals); + return QT; + } + + // Remove the part of the type related to the type being a template + // parameter (we won't report it as part of the 'type name' and it + // is actually make the code below to be more complex (to handle + // those) + while (isa<SubstTemplateTypeParmType>(QT.getTypePtr())) { + // Get the qualifiers. + Qualifiers Quals = QT.getQualifiers(); + + QT = cast<SubstTemplateTypeParmType>(QT.getTypePtr())->desugar(); + + // Add back the qualifiers. + QT = Ctx.getQualifiedType(QT, Quals); + } + + NestedNameSpecifier *Prefix = nullptr; + // Local qualifiers are attached to the QualType outside of the + // elaborated type. Retrieve them before descending into the + // elaborated type. + Qualifiers PrefixQualifiers = QT.getLocalQualifiers(); + QT = QualType(QT.getTypePtr(), 0); +#if LIBCLANG_VERSION_MAJOR >= 18 + constexpr ElaboratedTypeKeyword ETK_None = ElaboratedTypeKeyword::None; +#endif + ElaboratedTypeKeyword Keyword = ETK_None; + if (const auto *ETypeInput = dyn_cast<ElaboratedType>(QT.getTypePtr())) { + QT = ETypeInput->getNamedType(); + assert(!QT.hasLocalQualifiers()); + Keyword = ETypeInput->getKeyword(); + } + + // We don't consider the alias introduced by `using a::X` as a new type. + // The qualified name is still a::X. + if (const auto *UT = QT->getAs<UsingType>()) { + QT = Ctx.getQualifiedType(UT->getUnderlyingType(), PrefixQualifiers); + return getFullyQualifiedType(QT, Ctx, WithGlobalNsPrefix); + } + + // Create a nested name specifier if needed. + Prefix = createNestedNameSpecifierForScopeOf(Ctx, QT.getTypePtr(), + true /*FullyQualified*/, + WithGlobalNsPrefix); + + // In case of template specializations iterate over the arguments and + // fully qualify them as well. + if (isa<const TemplateSpecializationType>(QT.getTypePtr()) || + isa<const RecordType>(QT.getTypePtr())) { + // We are asked to fully qualify and we have a Record Type (which + // may point to a template specialization) or Template + // Specialization Type. We need to fully qualify their arguments. + + const Type *TypePtr = getFullyQualifiedTemplateType( + Ctx, QT.getTypePtr(), WithGlobalNsPrefix); + QT = QualType(TypePtr, 0); + } + if (Prefix || Keyword != ETK_None) { + QT = Ctx.getElaboratedType(Keyword, Prefix, QT); + } + QT = Ctx.getQualifiedType(QT, PrefixQualifiers); + return QT; +} + +inline std::string getFullyQualifiedName(QualType QT, + const ASTContext &Ctx, + const PrintingPolicy &Policy, + bool WithGlobalNsPrefix = false) { + QualType FQQT = getFullyQualifiedType(QT, Ctx, WithGlobalNsPrefix); + return FQQT.getAsString(Policy); +} + +} // end namespace TypeName +} // end namespace clang diff --git a/src/qdoc/qdoc/src/qdoc/clang/AST/qt_attribution.json b/src/qdoc/qdoc/src/qdoc/clang/AST/qt_attribution.json new file mode 100644 index 000000000..13219767d --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/clang/AST/qt_attribution.json @@ -0,0 +1,20 @@ +[ + { + "Id": "llvm_clang_typename_namespace", + "Name": "QualTypeNames", + "QDocModule": "qdoc", + "QtUsage": "Used to have access to a version of clang::TypeName::getFullyQualifiedName that does not expand templates to an instance.", + "QtParts": [ + "tools" + ], + "Files": "QualTypeNames.h", + + "Description": "A part of Clang's C++ API that deals with fully qualifying types.", + "Homepage": "https://github.com/llvm/llvm-project", + "Version": "16.0", + "License": "Apache License 2.0", + "LicenseId": "Apache-2.0 WITH LLVM-exception", + "LicenseFile": "LLVM_LICENSE.txt", + "Copyright": "Copyright assigned to LLVM project contributors." + } +] diff --git a/src/qdoc/qdoc/src/qdoc/clangcodeparser.cpp b/src/qdoc/qdoc/src/qdoc/clangcodeparser.cpp new file mode 100644 index 000000000..a414b55a3 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/clangcodeparser.cpp @@ -0,0 +1,1904 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "clangcodeparser.h" +#include "cppcodeparser.h" + +#include "access.h" +#include "classnode.h" +#include "codechunk.h" +#include "config.h" +#include "enumnode.h" +#include "functionnode.h" +#include "namespacenode.h" +#include "propertynode.h" +#include "qdocdatabase.h" +#include "typedefnode.h" +#include "variablenode.h" +#include "utilities.h" + +#include <QtCore/qdebug.h> +#include <QtCore/qelapsedtimer.h> +#include <QtCore/qfile.h> +#include <QtCore/qscopedvaluerollback.h> +#include <QtCore/qtemporarydir.h> +#include <QtCore/qtextstream.h> +#include <QtCore/qvarlengtharray.h> + +#include <clang-c/Index.h> + +#include <clang/AST/Decl.h> +#include <clang/AST/DeclFriend.h> +#include <clang/AST/DeclTemplate.h> +#include <clang/AST/Expr.h> +#include <clang/AST/Type.h> +#include <clang/AST/TypeLoc.h> +#include <clang/Basic/SourceLocation.h> +#include <clang/Frontend/ASTUnit.h> +#include <clang/Lex/Lexer.h> +#include <llvm/Support/Casting.h> + +#include "clang/AST/QualTypeNames.h" +#include "template_declaration.h" + +#include <cstdio> + +QT_BEGIN_NAMESPACE + +struct CompilationIndex { + CXIndex index = nullptr; + + operator CXIndex() { + return index; + } + + ~CompilationIndex() { + clang_disposeIndex(index); + } +}; + +struct TranslationUnit { + CXTranslationUnit tu = nullptr; + + operator CXTranslationUnit() { + return tu; + } + + operator bool() { + return tu; + } + + ~TranslationUnit() { + clang_disposeTranslationUnit(tu); + } +}; + +// We're printing diagnostics in ClangCodeParser::printDiagnostics, +// so avoid clang itself printing them. +static const auto kClangDontDisplayDiagnostics = 0; + +static CXTranslationUnit_Flags flags_ = static_cast<CXTranslationUnit_Flags>(0); + +constexpr const char fnDummyFileName[] = "/fn_dummyfile.cpp"; + +#ifndef QT_NO_DEBUG_STREAM +template<class T> +static QDebug operator<<(QDebug debug, const std::vector<T> &v) +{ + QDebugStateSaver saver(debug); + debug.noquote(); + debug.nospace(); + const size_t size = v.size(); + debug << "std::vector<>[" << size << "]("; + for (size_t i = 0; i < size; ++i) { + if (i) + debug << ", "; + debug << v[i]; + } + debug << ')'; + return debug; +} +#endif // !QT_NO_DEBUG_STREAM + +static void printDiagnostics(const CXTranslationUnit &translationUnit) +{ + if (!lcQdocClang().isDebugEnabled()) + return; + + static const auto displayOptions = CXDiagnosticDisplayOptions::CXDiagnostic_DisplaySourceLocation + | CXDiagnosticDisplayOptions::CXDiagnostic_DisplayColumn + | CXDiagnosticDisplayOptions::CXDiagnostic_DisplayOption; + + for (unsigned i = 0, numDiagnostics = clang_getNumDiagnostics(translationUnit); i < numDiagnostics; ++i) { + auto diagnostic = clang_getDiagnostic(translationUnit, i); + auto formattedDiagnostic = clang_formatDiagnostic(diagnostic, displayOptions); + qCDebug(lcQdocClang) << clang_getCString(formattedDiagnostic); + clang_disposeString(formattedDiagnostic); + clang_disposeDiagnostic(diagnostic); + } +} + +/*! + * Returns the underlying Decl that \a cursor represents. + * + * This can be used to drop back down from a LibClang's CXCursor to + * the underlying C++ AST that Clang provides. + * + * It should be used when LibClang does not expose certain + * functionalities that are available in the C++ AST. + * + * The CXCursor should represent a declaration. Usages of this + * function on CXCursors that do not represent a declaration may + * produce undefined results. + */ +static const clang::Decl* get_cursor_declaration(CXCursor cursor) { + assert(clang_isDeclaration(clang_getCursorKind(cursor))); + + return static_cast<const clang::Decl*>(cursor.data[0]); +} + + +/*! + * Returns a string representing the name of \a type as if it was + * referred to at the end of the translation unit that it was parsed + * from. + * + * For example, given the following code: + * + * \code + * namespace foo { + * template<typename T> + * struct Bar { + * using Baz = const T&; + * + * void bam(Baz); + * }; + * } + * \endcode + * + * Given a parsed translation unit and an AST node, say \e {decl}, + * representing the parameter declaration of the first argument of \c {bam}, + * calling \c{get_fully_qualified_name(decl->getType(), * decl->getASTContext())} + * would result in the string \c {foo::Bar<T>::Baz}. + * + * This should generally be used every time the stringified + * representation of a type is acquired as part of parsing with Clang, + * so as to ensure a consistent behavior and output. + */ +static std::string get_fully_qualified_type_name(clang::QualType type, const clang::ASTContext& declaration_context) { + return clang::TypeName::getFullyQualifiedName( + type, + declaration_context, + declaration_context.getPrintingPolicy() + ); +} + +/* + * Retrieves expression as written in the original source code. + * + * declaration_context should be the ASTContext of the declaration + * from which the expression was extracted from. + * + * If the expression contains a leading equal sign it will be removed. + * + * Leading and trailing spaces will be similarly removed from the expression. + */ +static std::string get_expression_as_string(const clang::Expr* expression, const clang::ASTContext& declaration_context) { + QString default_value = QString::fromStdString(clang::Lexer::getSourceText( + clang::CharSourceRange::getTokenRange(expression->getSourceRange()), + declaration_context.getSourceManager(), + declaration_context.getLangOpts() + ).str()); + + if (default_value.startsWith("=")) + default_value.remove(0, 1); + + default_value = default_value.trimmed(); + + return default_value.toStdString(); +} + +/* + * Retrieves the default value of the passed in type template parameter as a string. + * + * The default value of a type template parameter is always a type, + * and its stringified representation will be return as the fully + * qualified version of the type. + * + * If the parameter has no default value the empty string will be returned. + */ +static std::string get_default_value_initializer_as_string(const clang::TemplateTypeParmDecl* parameter) { + return (parameter && parameter->hasDefaultArgument()) ? + get_fully_qualified_type_name(parameter->getDefaultArgument(), parameter->getASTContext()) : + ""; + +} + +/* + * Retrieves the default value of the passed in non-type template parameter as a string. + * + * The default value of a non-type template parameter is an expression + * and its stringified representation will be return as it was written + * in the original code. + * + * If the parameter as no default value the empty string will be returned. + */ +static std::string get_default_value_initializer_as_string(const clang::NonTypeTemplateParmDecl* parameter) { + return (parameter && parameter->hasDefaultArgument()) ? + get_expression_as_string(parameter->getDefaultArgument(), parameter->getASTContext()) : ""; + +} + +/* + * Retrieves the default value of the passed in template template parameter as a string. + * + * The default value of a template template parameter is a template + * name and its stringified representation will be returned as a fully + * qualified version of that name. + * + * If the parameter as no default value the empty string will be returned. + */ +static std::string get_default_value_initializer_as_string(const clang::TemplateTemplateParmDecl* parameter) { + std::string default_value{}; + + if (parameter && parameter->hasDefaultArgument()) { + const clang::TemplateName template_name = parameter->getDefaultArgument().getArgument().getAsTemplate(); + + llvm::raw_string_ostream ss{default_value}; + template_name.print(ss, parameter->getASTContext().getPrintingPolicy(), clang::TemplateName::Qualified::Fully); + } + + return default_value; +} + +/* + * Retrieves the default value of the passed in function parameter as + * a string. + * + * The default value of a function parameter is an expression and its + * stringified representation will be returned as it was written in + * the original code. + * + * If the parameter as no default value or Clang was not able to yet + * parse it at this time the empty string will be returned. + */ +static std::string get_default_value_initializer_as_string(const clang::ParmVarDecl* parameter) { + if (!parameter || !parameter->hasDefaultArg() || parameter->hasUnparsedDefaultArg()) + return ""; + + return get_expression_as_string( + parameter->hasUninstantiatedDefaultArg() ? parameter->getUninstantiatedDefaultArg() : parameter->getDefaultArg(), + parameter->getASTContext() + ); +} + +/* + * Retrieves the default value of the passed in declaration, based on + * its concrete type, as a string. + * + * If the declaration is a nullptr or the concrete type of the + * declaration is not a supported one, the returned string will be the + * empty string. + */ +static std::string get_default_value_initializer_as_string(const clang::NamedDecl* declaration) { + if (!declaration) return ""; + + if (auto type_template_parameter = llvm::dyn_cast<clang::TemplateTypeParmDecl>(declaration)) + return get_default_value_initializer_as_string(type_template_parameter); + + if (auto non_type_template_parameter = llvm::dyn_cast<clang::NonTypeTemplateParmDecl>(declaration)) + return get_default_value_initializer_as_string(non_type_template_parameter); + + if (auto template_template_parameter = llvm::dyn_cast<clang::TemplateTemplateParmDecl>(declaration)) { + return get_default_value_initializer_as_string(template_template_parameter); + } + + if (auto function_parameter = llvm::dyn_cast<clang::ParmVarDecl>(declaration)) { + return get_default_value_initializer_as_string(function_parameter); + } + + return ""; +} + +/*! + Call clang_visitChildren on the given cursor with the lambda as a callback + T can be any functor that is callable with a CXCursor parameter and returns a CXChildVisitResult + (in other word compatible with function<CXChildVisitResult(CXCursor)> + */ +template<typename T> +bool visitChildrenLambda(CXCursor cursor, T &&lambda) +{ + CXCursorVisitor visitor = [](CXCursor c, CXCursor, + CXClientData client_data) -> CXChildVisitResult { + return (*static_cast<T *>(client_data))(c); + }; + return clang_visitChildren(cursor, visitor, &lambda); +} + +/*! + convert a CXString to a QString, and dispose the CXString + */ +static QString fromCXString(CXString &&string) +{ + QString ret = QString::fromUtf8(clang_getCString(string)); + clang_disposeString(string); + return ret; +} + +/* + * Returns an intermediate representation that models the the given + * template declaration. + */ +static RelaxedTemplateDeclaration get_template_declaration(const clang::TemplateDecl* template_declaration) { + assert(template_declaration); + + RelaxedTemplateDeclaration template_declaration_ir{}; + + auto template_parameters = template_declaration->getTemplateParameters(); + for (auto template_parameter : template_parameters->asArray()) { + auto kind{RelaxedTemplateParameter::Kind::TypeTemplateParameter}; + std::string type{}; + + if (auto non_type_template_parameter = llvm::dyn_cast<clang::NonTypeTemplateParmDecl>(template_parameter)) { + kind = RelaxedTemplateParameter::Kind::NonTypeTemplateParameter; + type = get_fully_qualified_type_name(non_type_template_parameter->getType(), non_type_template_parameter->getASTContext()); + + // REMARK: QDoc uses this information to match a user + // provided documentation (for example from an "\fn" + // command) with a `Node` that was extracted from the + // code-base. + // + // Due to how QDoc obtains an AST for documentation that + // is provided by the user, there might be a mismatch in + // the type of certain non type template parameters. + // + // QDoc generally builds a fake out-of-line definition for + // a callable provided through an "\fn" command, when it + // needs to match it. + // In that context, certain type names may be dependent + // names, while they may not be when the element they + // represent is extracted from the code-base. + // + // This in turn makes their stringified representation + // different in the two contextes, as a dependent name may + // require the "typename" keyword to precede it. + // + // Since QDoc uses a very simplified model, and it + // generally doesn't need care about the exact name + // resolution rules for C++, since it passes by + // Clang-validated data, we remove the "typename" keyword + // if it prefixes the type representation, so that it + // doesn't impact the matching procedure.. + + // KLUDGE: Waiting for C++20 to avoid the conversion. + // Doesn't really impact performance in a + // meaningful way so it can be kept while waiting. + if (QString::fromStdString(type).startsWith("typename ")) type.erase(0, std::string("typename ").size()); + } + + auto template_template_parameter = llvm::dyn_cast<clang::TemplateTemplateParmDecl>(template_parameter); + if (template_template_parameter) kind = RelaxedTemplateParameter::Kind::TemplateTemplateParameter; + + template_declaration_ir.parameters.push_back({ + kind, + template_parameter->isTemplateParameterPack(), + { + type, + template_parameter->getNameAsString(), + get_default_value_initializer_as_string(template_parameter) + }, + (template_template_parameter ? + std::optional<TemplateDeclarationStorage>(TemplateDeclarationStorage{ + get_template_declaration(template_template_parameter).parameters + }) : std::nullopt) + }); + } + + return template_declaration_ir; +} + +/*! + convert a CXSourceLocation to a qdoc Location + */ +static Location fromCXSourceLocation(CXSourceLocation location) +{ + unsigned int line, column; + CXString file; + clang_getPresumedLocation(location, &file, &line, &column); + Location l(fromCXString(std::move(file))); + l.setColumnNo(column); + l.setLineNo(line); + return l; +} + +/*! + convert a CX_CXXAccessSpecifier to Node::Access + */ +static Access fromCX_CXXAccessSpecifier(CX_CXXAccessSpecifier spec) +{ + switch (spec) { + case CX_CXXPrivate: + return Access::Private; + case CX_CXXProtected: + return Access::Protected; + case CX_CXXPublic: + return Access::Public; + default: + return Access::Public; + } +} + +/*! + Returns the spelling in the file for a source range + */ + +struct FileCacheEntry +{ + QByteArray fileName; + QByteArray content; +}; + +static inline QString fromCache(const QByteArray &cache, + unsigned int offset1, unsigned int offset2) +{ + return QString::fromUtf8(cache.mid(offset1, offset2 - offset1)); +} + +static QString readFile(CXFile cxFile, unsigned int offset1, unsigned int offset2) +{ + using FileCache = QList<FileCacheEntry>; + static FileCache cache; + + CXString cxFileName = clang_getFileName(cxFile); + const QByteArray fileName = clang_getCString(cxFileName); + clang_disposeString(cxFileName); + + for (const auto &entry : std::as_const(cache)) { + if (fileName == entry.fileName) + return fromCache(entry.content, offset1, offset2); + } + + QFile file(QString::fromUtf8(fileName)); + if (file.open(QIODeviceBase::ReadOnly)) { // binary to match clang offsets + FileCacheEntry entry{fileName, file.readAll()}; + cache.prepend(entry); + while (cache.size() > 5) + cache.removeLast(); + return fromCache(entry.content, offset1, offset2); + } + return {}; +} + +static QString getSpelling(CXSourceRange range) +{ + auto start = clang_getRangeStart(range); + auto end = clang_getRangeEnd(range); + CXFile file1, file2; + unsigned int offset1, offset2; + clang_getFileLocation(start, &file1, nullptr, nullptr, &offset1); + clang_getFileLocation(end, &file2, nullptr, nullptr, &offset2); + + if (file1 != file2 || offset2 <= offset1) + return QString(); + + return readFile(file1, offset1, offset2); +} + +/*! + Returns the function name from a given cursor representing a + function declaration. This is usually clang_getCursorSpelling, but + not for the conversion function in which case it is a bit more complicated + */ +QString functionName(CXCursor cursor) +{ + if (clang_getCursorKind(cursor) == CXCursor_ConversionFunction) { + // For a CXCursor_ConversionFunction we don't want the spelling which would be something + // like "operator type-parameter-0-0" or "operator unsigned int". we want the actual name as + // spelled; + auto conversion_declaration = + static_cast<const clang::CXXConversionDecl*>(get_cursor_declaration(cursor)); + + return QLatin1String("operator ") + QString::fromStdString(get_fully_qualified_type_name( + conversion_declaration->getConversionType(), + conversion_declaration->getASTContext() + )); + } + + QString name = fromCXString(clang_getCursorSpelling(cursor)); + + // Remove template stuff from constructor and destructor but not from operator< + auto ltLoc = name.indexOf('<'); + if (ltLoc > 0 && !name.startsWith("operator<")) + name = name.left(ltLoc); + return name; +} + +/*! + Reconstruct the qualified path name of a function that is + being overridden. + */ +static QString reconstructQualifiedPathForCursor(CXCursor cur) +{ + QString path; + auto kind = clang_getCursorKind(cur); + while (!clang_isInvalid(kind) && kind != CXCursor_TranslationUnit) { + switch (kind) { + case CXCursor_Namespace: + case CXCursor_StructDecl: + case CXCursor_ClassDecl: + case CXCursor_UnionDecl: + case CXCursor_ClassTemplate: + path.prepend("::"); + path.prepend(fromCXString(clang_getCursorSpelling(cur))); + break; + case CXCursor_FunctionDecl: + case CXCursor_FunctionTemplate: + case CXCursor_CXXMethod: + case CXCursor_Constructor: + case CXCursor_Destructor: + case CXCursor_ConversionFunction: + path = functionName(cur); + break; + default: + break; + } + cur = clang_getCursorSemanticParent(cur); + kind = clang_getCursorKind(cur); + } + return path; +} + +/*! + Find the node from the QDocDatabase \a qdb that corrseponds to the declaration + represented by the cursor \a cur, if it exists. + */ +static Node *findNodeForCursor(QDocDatabase *qdb, CXCursor cur) +{ + auto kind = clang_getCursorKind(cur); + if (clang_isInvalid(kind)) + return nullptr; + if (kind == CXCursor_TranslationUnit) + return qdb->primaryTreeRoot(); + + Node *p = findNodeForCursor(qdb, clang_getCursorSemanticParent(cur)); + if (p == nullptr) + return nullptr; + if (!p->isAggregate()) + return nullptr; + auto parent = static_cast<Aggregate *>(p); + + QString name = fromCXString(clang_getCursorSpelling(cur)); + switch (kind) { + case CXCursor_Namespace: + return parent->findNonfunctionChild(name, &Node::isNamespace); + case CXCursor_StructDecl: + case CXCursor_ClassDecl: + case CXCursor_UnionDecl: + case CXCursor_ClassTemplate: + return parent->findNonfunctionChild(name, &Node::isClassNode); + case CXCursor_FunctionDecl: + case CXCursor_FunctionTemplate: + case CXCursor_CXXMethod: + case CXCursor_Constructor: + case CXCursor_Destructor: + case CXCursor_ConversionFunction: { + NodeVector candidates; + parent->findChildren(functionName(cur), candidates); + if (candidates.isEmpty()) + return nullptr; + + CXType funcType = clang_getCursorType(cur); + auto numArg = clang_getNumArgTypes(funcType); + bool isVariadic = clang_isFunctionTypeVariadic(funcType); + QVarLengthArray<QString, 20> args; + + std::optional<RelaxedTemplateDeclaration> relaxed_template_declaration{std::nullopt}; + if (kind == CXCursor_FunctionTemplate) + relaxed_template_declaration = get_template_declaration( + get_cursor_declaration(cur)->getAsFunction()->getDescribedFunctionTemplate() + ); + + for (Node *candidate : std::as_const(candidates)) { + if (!candidate->isFunction(Node::CPP)) + continue; + + auto fn = static_cast<FunctionNode *>(candidate); + + if (!fn->templateDecl() && relaxed_template_declaration) + continue; + + if (fn->templateDecl() && !relaxed_template_declaration) + continue; + + if (fn->templateDecl() && relaxed_template_declaration && + !are_template_declarations_substitutable(*fn->templateDecl(), *relaxed_template_declaration)) + continue; + + const Parameters ¶meters = fn->parameters(); + + if (parameters.count() != numArg + isVariadic) + continue; + + if (fn->isConst() != bool(clang_CXXMethod_isConst(cur))) + continue; + + if (isVariadic && parameters.last().type() != QLatin1String("...")) + continue; + + if (fn->isRef() != (clang_Type_getCXXRefQualifier(funcType) == CXRefQualifier_LValue)) + continue; + + if (fn->isRefRef() != (clang_Type_getCXXRefQualifier(funcType) == CXRefQualifier_RValue)) + continue; + + auto function_declaration = get_cursor_declaration(cur)->getAsFunction(); + + bool different = false; + for (int i = 0; i < numArg; ++i) { + CXType argType = clang_getArgType(funcType, i); + + if (args.size() <= i) + args.append(QString::fromStdString(get_fully_qualified_type_name( + function_declaration->getParamDecl(i)->getOriginalType(), + function_declaration->getASTContext() + ))); + + QString recordedType = parameters.at(i).type(); + QString typeSpelling = args.at(i); + + different = recordedType != typeSpelling; + + // Retry with a canonical type spelling + if (different && (argType.kind == CXType_Typedef || argType.kind == CXType_Elaborated)) { + QStringView canonicalType = parameters.at(i).canonicalType(); + if (!canonicalType.isEmpty()) { + different = canonicalType != + QString::fromStdString(get_fully_qualified_type_name( + function_declaration->getParamDecl(i)->getOriginalType().getCanonicalType(), + function_declaration->getASTContext() + )); + } + } + + if (different) { + break; + } + } + + if (!different) + return fn; + } + return nullptr; + } + case CXCursor_EnumDecl: + return parent->findNonfunctionChild(name, &Node::isEnumType); + case CXCursor_FieldDecl: + case CXCursor_VarDecl: + return parent->findNonfunctionChild(name, &Node::isVariable); + case CXCursor_TypedefDecl: + return parent->findNonfunctionChild(name, &Node::isTypedef); + default: + return nullptr; + } +} + +static void setOverridesForFunction(FunctionNode *fn, CXCursor cursor) +{ + CXCursor *overridden; + unsigned int numOverridden = 0; + clang_getOverriddenCursors(cursor, &overridden, &numOverridden); + for (uint i = 0; i < numOverridden; ++i) { + QString path = reconstructQualifiedPathForCursor(overridden[i]); + if (!path.isEmpty()) { + fn->setOverride(true); + fn->setOverridesThis(path); + break; + } + } + clang_disposeOverriddenCursors(overridden); +} + +class ClangVisitor +{ +public: + ClangVisitor(QDocDatabase *qdb, const std::set<Config::HeaderFilePath> &allHeaders) + : qdb_(qdb), parent_(qdb->primaryTreeRoot()) + { + std::transform(allHeaders.cbegin(), allHeaders.cend(), std::inserter(allHeaders_, allHeaders_.begin()), + [](const auto& header_file_path) { return header_file_path.filename; }); + } + + QDocDatabase *qdocDB() { return qdb_; } + + CXChildVisitResult visitChildren(CXCursor cursor) + { + auto ret = visitChildrenLambda(cursor, [&](CXCursor cur) { + auto loc = clang_getCursorLocation(cur); + if (clang_Location_isFromMainFile(loc)) + return visitSource(cur, loc); + CXFile file; + clang_getFileLocation(loc, &file, nullptr, nullptr, nullptr); + bool isInteresting = false; + auto it = isInterestingCache_.find(file); + if (it != isInterestingCache_.end()) { + isInteresting = *it; + } else { + QFileInfo fi(fromCXString(clang_getFileName(file))); + // Match by file name in case of PCH/installed headers + isInteresting = allHeaders_.find(fi.fileName()) != allHeaders_.end(); + isInterestingCache_[file] = isInteresting; + } + if (isInteresting) { + return visitHeader(cur, loc); + } + + return CXChildVisit_Continue; + }); + return ret ? CXChildVisit_Break : CXChildVisit_Continue; + } + + /* + Not sure about all the possibilities, when the cursor + location is not in the main file. + */ + CXChildVisitResult visitFnArg(CXCursor cursor, Node **fnNode, bool &ignoreSignature) + { + auto ret = visitChildrenLambda(cursor, [&](CXCursor cur) { + auto loc = clang_getCursorLocation(cur); + if (clang_Location_isFromMainFile(loc)) + return visitFnSignature(cur, loc, fnNode, ignoreSignature); + return CXChildVisit_Continue; + }); + return ret ? CXChildVisit_Break : CXChildVisit_Continue; + } + + Node *nodeForCommentAtLocation(CXSourceLocation loc, CXSourceLocation nextCommentLoc); + +private: + /*! + SimpleLoc represents a simple location in the main source file, + which can be used as a key in a QMap. + */ + struct SimpleLoc + { + unsigned int line {}, column {}; + friend bool operator<(const SimpleLoc &a, const SimpleLoc &b) + { + return a.line != b.line ? a.line < b.line : a.column < b.column; + } + }; + /*! + \variable ClangVisitor::declMap_ + Map of all the declarations in the source file so we can match them + with a documentation comment. + */ + QMap<SimpleLoc, CXCursor> declMap_; + + QDocDatabase *qdb_; + Aggregate *parent_; + std::set<QString> allHeaders_; + QHash<CXFile, bool> isInterestingCache_; // doing a canonicalFilePath is slow, so keep a cache. + + /*! + Returns true if the symbol should be ignored for the documentation. + */ + bool ignoredSymbol(const QString &symbolName) + { + if (symbolName == QLatin1String("QPrivateSignal")) + return true; + // Ignore functions generated by property macros + if (symbolName.startsWith("_qt_property_")) + return true; + // Ignore template argument deduction guides + if (symbolName.startsWith("<deduction guide")) + return true; + return false; + } + + CXChildVisitResult visitSource(CXCursor cursor, CXSourceLocation loc); + CXChildVisitResult visitHeader(CXCursor cursor, CXSourceLocation loc); + CXChildVisitResult visitFnSignature(CXCursor cursor, CXSourceLocation loc, Node **fnNode, + bool &ignoreSignature); + void processFunction(FunctionNode *fn, CXCursor cursor); + bool parseProperty(const QString &spelling, const Location &loc); + void readParameterNamesAndAttributes(FunctionNode *fn, CXCursor cursor); + Aggregate *getSemanticParent(CXCursor cursor); +}; + +/*! + Visits a cursor in the .cpp file. + This fills the declMap_ + */ +CXChildVisitResult ClangVisitor::visitSource(CXCursor cursor, CXSourceLocation loc) +{ + auto kind = clang_getCursorKind(cursor); + if (clang_isDeclaration(kind)) { + SimpleLoc l; + clang_getPresumedLocation(loc, nullptr, &l.line, &l.column); + declMap_.insert(l, cursor); + return CXChildVisit_Recurse; + } + return CXChildVisit_Continue; +} + +/*! + If the semantic and lexical parent cursors of \a cursor are + not the same, find the Aggregate node for the semantic parent + cursor and return it. Otherwise return the current parent. + */ +Aggregate *ClangVisitor::getSemanticParent(CXCursor cursor) +{ + CXCursor sp = clang_getCursorSemanticParent(cursor); + CXCursor lp = clang_getCursorLexicalParent(cursor); + if (!clang_equalCursors(sp, lp) && clang_isDeclaration(clang_getCursorKind(sp))) { + Node *spn = findNodeForCursor(qdb_, sp); + if (spn && spn->isAggregate()) { + return static_cast<Aggregate *>(spn); + } + } + return parent_; +} + +CXChildVisitResult ClangVisitor::visitFnSignature(CXCursor cursor, CXSourceLocation, Node **fnNode, + bool &ignoreSignature) +{ + switch (clang_getCursorKind(cursor)) { + case CXCursor_Namespace: + return CXChildVisit_Recurse; + case CXCursor_FunctionDecl: + case CXCursor_FunctionTemplate: + case CXCursor_CXXMethod: + case CXCursor_Constructor: + case CXCursor_Destructor: + case CXCursor_ConversionFunction: { + ignoreSignature = false; + if (ignoredSymbol(functionName(cursor))) { + *fnNode = nullptr; + ignoreSignature = true; + } else { + *fnNode = findNodeForCursor(qdb_, cursor); + if (*fnNode) { + if ((*fnNode)->isFunction(Node::CPP)) { + auto *fn = static_cast<FunctionNode *>(*fnNode); + readParameterNamesAndAttributes(fn, cursor); + } + } else { // Possibly an implicitly generated special member + QString name = functionName(cursor); + if (ignoredSymbol(name)) + return CXChildVisit_Continue; + Aggregate *semanticParent = getSemanticParent(cursor); + if (semanticParent && semanticParent->isClass()) { + auto *candidate = new FunctionNode(nullptr, name); + processFunction(candidate, cursor); + if (!candidate->isSpecialMemberFunction()) { + delete candidate; + return CXChildVisit_Continue; + } + candidate->setDefault(true); + semanticParent->addChild(*fnNode = candidate); + } + } + } + break; + } + default: + break; + } + return CXChildVisit_Continue; +} + +CXChildVisitResult ClangVisitor::visitHeader(CXCursor cursor, CXSourceLocation loc) +{ + auto kind = clang_getCursorKind(cursor); + + switch (kind) { + case CXCursor_TypeAliasTemplateDecl: + case CXCursor_TypeAliasDecl: { + QString aliasDecl = getSpelling(clang_getCursorExtent(cursor)).simplified(); + QStringList typeAlias = aliasDecl.split(QLatin1Char('=')); + if (typeAlias.size() == 2) { + typeAlias[0] = typeAlias[0].trimmed(); + const QLatin1String usingString("using "); + qsizetype usingPos = typeAlias[0].indexOf(usingString); + if (usingPos != -1) { + typeAlias[0].remove(0, usingPos + usingString.size()); + typeAlias[0] = typeAlias[0].split(QLatin1Char(' ')).first(); + typeAlias[1] = typeAlias[1].trimmed(); + auto *ta = new TypeAliasNode(parent_, typeAlias[0], typeAlias[1]); + ta->setAccess(fromCX_CXXAccessSpecifier(clang_getCXXAccessSpecifier(cursor))); + ta->setLocation(fromCXSourceLocation(clang_getCursorLocation(cursor))); + + if (kind == CXCursor_TypeAliasTemplateDecl) { + auto template_decl = llvm::dyn_cast<clang::TemplateDecl>(get_cursor_declaration(cursor)); + ta->setTemplateDecl(get_template_declaration(template_decl)); + } + } + } + return CXChildVisit_Continue; + } + case CXCursor_StructDecl: + case CXCursor_UnionDecl: + if (fromCXString(clang_getCursorSpelling(cursor)).isEmpty()) // anonymous struct or union + return CXChildVisit_Continue; + Q_FALLTHROUGH(); + case CXCursor_ClassTemplate: + Q_FALLTHROUGH(); + case CXCursor_ClassDecl: { + if (!clang_isCursorDefinition(cursor)) + return CXChildVisit_Continue; + + if (findNodeForCursor(qdb_, cursor)) // Was already parsed, probably in another TU + return CXChildVisit_Continue; + + QString className = fromCXString(clang_getCursorSpelling(cursor)); + + Aggregate *semanticParent = getSemanticParent(cursor); + if (semanticParent && semanticParent->findNonfunctionChild(className, &Node::isClassNode)) { + return CXChildVisit_Continue; + } + + CXCursorKind actualKind = (kind == CXCursor_ClassTemplate) ? + clang_getTemplateCursorKind(cursor) : kind; + + Node::NodeType type = Node::Class; + if (actualKind == CXCursor_StructDecl) + type = Node::Struct; + else if (actualKind == CXCursor_UnionDecl) + type = Node::Union; + + auto *classe = new ClassNode(type, semanticParent, className); + classe->setAccess(fromCX_CXXAccessSpecifier(clang_getCXXAccessSpecifier(cursor))); + classe->setLocation(fromCXSourceLocation(clang_getCursorLocation(cursor))); + + if (kind == CXCursor_ClassTemplate) { + auto template_declaration = llvm::dyn_cast<clang::TemplateDecl>(get_cursor_declaration(cursor)); + classe->setTemplateDecl(get_template_declaration(template_declaration)); + } + + QScopedValueRollback<Aggregate *> setParent(parent_, classe); + return visitChildren(cursor); + } + case CXCursor_CXXBaseSpecifier: { + if (!parent_->isClassNode()) + return CXChildVisit_Continue; + auto access = fromCX_CXXAccessSpecifier(clang_getCXXAccessSpecifier(cursor)); + auto type = clang_getCursorType(cursor); + auto baseCursor = clang_getTypeDeclaration(type); + auto baseNode = findNodeForCursor(qdb_, baseCursor); + auto classe = static_cast<ClassNode *>(parent_); + if (baseNode == nullptr || !baseNode->isClassNode()) { + QString bcName = reconstructQualifiedPathForCursor(baseCursor); + classe->addUnresolvedBaseClass(access, + bcName.split(QLatin1String("::"), Qt::SkipEmptyParts)); + return CXChildVisit_Continue; + } + auto baseClasse = static_cast<ClassNode *>(baseNode); + classe->addResolvedBaseClass(access, baseClasse); + return CXChildVisit_Continue; + } + case CXCursor_Namespace: { + QString namespaceName = fromCXString(clang_getCursorDisplayName(cursor)); + NamespaceNode *ns = nullptr; + if (parent_) + ns = static_cast<NamespaceNode *>( + parent_->findNonfunctionChild(namespaceName, &Node::isNamespace)); + if (!ns) { + ns = new NamespaceNode(parent_, namespaceName); + ns->setAccess(Access::Public); + ns->setLocation(fromCXSourceLocation(clang_getCursorLocation(cursor))); + } + QScopedValueRollback<Aggregate *> setParent(parent_, ns); + return visitChildren(cursor); + } + case CXCursor_FunctionTemplate: + Q_FALLTHROUGH(); + case CXCursor_FunctionDecl: + case CXCursor_CXXMethod: + case CXCursor_Constructor: + case CXCursor_Destructor: + case CXCursor_ConversionFunction: { + if (findNodeForCursor(qdb_, cursor)) // Was already parsed, probably in another TU + return CXChildVisit_Continue; + QString name = functionName(cursor); + if (ignoredSymbol(name)) + return CXChildVisit_Continue; + // constexpr constructors generate also a global instance; ignore + if (kind == CXCursor_Constructor && parent_ == qdb_->primaryTreeRoot()) + return CXChildVisit_Continue; + + auto *fn = new FunctionNode(parent_, name); + CXSourceRange range = clang_Cursor_getCommentRange(cursor); + if (!clang_Range_isNull(range)) { + QString comment = getSpelling(range); + if (comment.startsWith("//!")) { + qsizetype tag = comment.indexOf(QChar('[')); + if (tag > 0) { + qsizetype end = comment.indexOf(QChar(']'), ++tag); + if (end > 0) + fn->setTag(comment.mid(tag, end - tag)); + } + } + } + + processFunction(fn, cursor); + + if (kind == CXCursor_FunctionTemplate) { + auto template_declaration = get_cursor_declaration(cursor)->getAsFunction()->getDescribedFunctionTemplate(); + fn->setTemplateDecl(get_template_declaration(template_declaration)); + } + + return CXChildVisit_Continue; + } +#if CINDEX_VERSION >= 36 + case CXCursor_FriendDecl: { + return visitChildren(cursor); + } +#endif + case CXCursor_EnumDecl: { + auto *en = static_cast<EnumNode *>(findNodeForCursor(qdb_, cursor)); + if (en && en->items().size()) + return CXChildVisit_Continue; // Was already parsed, probably in another TU + + QString enumTypeName = fromCXString(clang_getCursorSpelling(cursor)); + + if (clang_Cursor_isAnonymous(cursor)) { + enumTypeName = "anonymous"; + if (parent_ && (parent_->isClassNode() || parent_->isNamespace())) { + Node *n = parent_->findNonfunctionChild(enumTypeName, &Node::isEnumType); + if (n) + en = static_cast<EnumNode *>(n); + } + } + if (!en) { + en = new EnumNode(parent_, enumTypeName, clang_EnumDecl_isScoped(cursor)); + en->setAccess(fromCX_CXXAccessSpecifier(clang_getCXXAccessSpecifier(cursor))); + en->setLocation(fromCXSourceLocation(clang_getCursorLocation(cursor))); + } + + // Enum values + visitChildrenLambda(cursor, [&](CXCursor cur) { + if (clang_getCursorKind(cur) != CXCursor_EnumConstantDecl) + return CXChildVisit_Continue; + + QString value; + visitChildrenLambda(cur, [&](CXCursor cur) { + if (clang_isExpression(clang_getCursorKind(cur))) { + value = getSpelling(clang_getCursorExtent(cur)); + return CXChildVisit_Break; + } + return CXChildVisit_Continue; + }); + if (value.isEmpty()) { + QLatin1String hex("0x"); + if (!en->items().isEmpty() && en->items().last().value().startsWith(hex)) { + value = hex + QString::number(clang_getEnumConstantDeclValue(cur), 16); + } else { + value = QString::number(clang_getEnumConstantDeclValue(cur)); + } + } + + en->addItem(EnumItem(fromCXString(clang_getCursorSpelling(cur)), value)); + return CXChildVisit_Continue; + }); + return CXChildVisit_Continue; + } + case CXCursor_FieldDecl: + case CXCursor_VarDecl: { + if (findNodeForCursor(qdb_, cursor)) // Was already parsed, probably in another TU + return CXChildVisit_Continue; + + auto value_declaration = + llvm::dyn_cast<clang::ValueDecl>(get_cursor_declaration(cursor)); + assert(value_declaration); + + auto access = fromCX_CXXAccessSpecifier(clang_getCXXAccessSpecifier(cursor)); + auto var = new VariableNode(parent_, fromCXString(clang_getCursorSpelling(cursor))); + + var->setAccess(access); + var->setLocation(fromCXSourceLocation(clang_getCursorLocation(cursor))); + var->setLeftType(QString::fromStdString(get_fully_qualified_type_name( + value_declaration->getType(), + value_declaration->getASTContext() + ))); + var->setStatic(kind == CXCursor_VarDecl && parent_->isClassNode()); + + return CXChildVisit_Continue; + } + case CXCursor_TypedefDecl: { + if (findNodeForCursor(qdb_, cursor)) // Was already parsed, probably in another TU + return CXChildVisit_Continue; + auto *td = new TypedefNode(parent_, fromCXString(clang_getCursorSpelling(cursor))); + td->setAccess(fromCX_CXXAccessSpecifier(clang_getCXXAccessSpecifier(cursor))); + td->setLocation(fromCXSourceLocation(clang_getCursorLocation(cursor))); + // Search to see if this is a Q_DECLARE_FLAGS (if the type is QFlags<ENUM>) + visitChildrenLambda(cursor, [&](CXCursor cur) { + if (clang_getCursorKind(cur) != CXCursor_TemplateRef + || fromCXString(clang_getCursorSpelling(cur)) != QLatin1String("QFlags")) + return CXChildVisit_Continue; + // Found QFlags<XXX> + visitChildrenLambda(cursor, [&](CXCursor cur) { + if (clang_getCursorKind(cur) != CXCursor_TypeRef) + return CXChildVisit_Continue; + auto *en = + findNodeForCursor(qdb_, clang_getTypeDeclaration(clang_getCursorType(cur))); + if (en && en->isEnumType()) + static_cast<EnumNode *>(en)->setFlagsType(td); + return CXChildVisit_Break; + }); + return CXChildVisit_Break; + }); + return CXChildVisit_Continue; + } + default: + if (clang_isDeclaration(kind) && parent_->isClassNode()) { + // may be a property macro or a static_assert + // which is not exposed from the clang API + parseProperty(getSpelling(clang_getCursorExtent(cursor)), + fromCXSourceLocation(loc)); + } + return CXChildVisit_Continue; + } +} + +void ClangVisitor::readParameterNamesAndAttributes(FunctionNode *fn, CXCursor cursor) +{ + Parameters ¶meters = fn->parameters(); + // Visit the parameters and attributes + int i = 0; + visitChildrenLambda(cursor, [&](CXCursor cur) { + auto kind = clang_getCursorKind(cur); + if (kind == CXCursor_AnnotateAttr) { + QString annotation = fromCXString(clang_getCursorDisplayName(cur)); + if (annotation == QLatin1String("qt_slot")) { + fn->setMetaness(FunctionNode::Slot); + } else if (annotation == QLatin1String("qt_signal")) { + fn->setMetaness(FunctionNode::Signal); + } + if (annotation == QLatin1String("qt_invokable")) + fn->setInvokable(true); + } else if (kind == CXCursor_CXXOverrideAttr) { + fn->setOverride(true); + } else if (kind == CXCursor_ParmDecl) { + if (i >= parameters.count()) + return CXChildVisit_Break; // Attributes comes before parameters so we can break. + + if (QString name = fromCXString(clang_getCursorSpelling(cur)); !name.isEmpty()) + parameters[i].setName(name); + + const clang::ParmVarDecl* parameter_declaration = llvm::dyn_cast<const clang::ParmVarDecl>(get_cursor_declaration(cur)); + Q_ASSERT(parameter_declaration); + + std::string default_value = get_default_value_initializer_as_string(parameter_declaration); + + if (!default_value.empty()) + parameters[i].setDefaultValue(QString::fromStdString(default_value)); + + ++i; + } + return CXChildVisit_Continue; + }); +} + +void ClangVisitor::processFunction(FunctionNode *fn, CXCursor cursor) +{ + CXCursorKind kind = clang_getCursorKind(cursor); + CXType funcType = clang_getCursorType(cursor); + fn->setAccess(fromCX_CXXAccessSpecifier(clang_getCXXAccessSpecifier(cursor))); + fn->setLocation(fromCXSourceLocation(clang_getCursorLocation(cursor))); + fn->setStatic(clang_CXXMethod_isStatic(cursor)); + fn->setConst(clang_CXXMethod_isConst(cursor)); + fn->setVirtualness(!clang_CXXMethod_isVirtual(cursor) + ? FunctionNode::NonVirtual + : clang_CXXMethod_isPureVirtual(cursor) + ? FunctionNode::PureVirtual + : FunctionNode::NormalVirtual); + + // REMARK: We assume that the following operations and casts are + // generally safe. + // Callers of those methods will generally check at the LibClang + // level the kind of cursor we are dealing with and will pass on + // only valid cursors that are of a function kind and that are at + // least a declaration. + // + // Failure to do so implies a bug in the call chain and should be + // dealt with as such. + const clang::Decl* declaration = get_cursor_declaration(cursor); + + assert(declaration); + + const clang::FunctionDecl* function_declaration = declaration->getAsFunction(); + + if (kind == CXCursor_Constructor + // a constructor template is classified as CXCursor_FunctionTemplate + || (kind == CXCursor_FunctionTemplate && fn->name() == parent_->name())) + fn->setMetaness(FunctionNode::Ctor); + else if (kind == CXCursor_Destructor) + fn->setMetaness(FunctionNode::Dtor); + else + fn->setReturnType(QString::fromStdString(get_fully_qualified_type_name( + function_declaration->getReturnType(), + function_declaration->getASTContext() + ))); + + const clang::CXXConstructorDecl* constructor_declaration = llvm::dyn_cast<const clang::CXXConstructorDecl>(function_declaration); + + if (constructor_declaration && constructor_declaration->isCopyConstructor()) fn->setMetaness(FunctionNode::CCtor); + else if (constructor_declaration && constructor_declaration->isMoveConstructor()) fn->setMetaness(FunctionNode::MCtor); + + const clang::CXXConversionDecl* conversion_declaration = llvm::dyn_cast<const clang::CXXConversionDecl>(function_declaration); + + if (function_declaration->isConstexpr()) fn->markConstexpr(); + if ( + (constructor_declaration && constructor_declaration->isExplicit()) || + (conversion_declaration && conversion_declaration->isExplicit()) + ) fn->markExplicit(); + + const clang::CXXMethodDecl* method_declaration = llvm::dyn_cast<const clang::CXXMethodDecl>(function_declaration); + + if (method_declaration && method_declaration->isCopyAssignmentOperator()) fn->setMetaness(FunctionNode::CAssign); + else if (method_declaration && method_declaration->isMoveAssignmentOperator()) fn->setMetaness(FunctionNode::MAssign); + + const clang::FunctionType* function_type = function_declaration->getFunctionType(); + const clang::FunctionProtoType* function_prototype = static_cast<const clang::FunctionProtoType*>(function_type); + + if (function_prototype) { + clang::FunctionProtoType::ExceptionSpecInfo exception_specification = function_prototype->getExceptionSpecInfo(); + + if (exception_specification.Type != clang::ExceptionSpecificationType::EST_None) { + const std::string exception_specification_spelling = + exception_specification.NoexceptExpr ? get_expression_as_string( + exception_specification.NoexceptExpr, + function_declaration->getASTContext() + ) : ""; + + if (exception_specification_spelling != "false") + fn->markNoexcept(QString::fromStdString(exception_specification_spelling)); + } + } + + CXRefQualifierKind refQualKind = clang_Type_getCXXRefQualifier(funcType); + if (refQualKind == CXRefQualifier_LValue) + fn->setRef(true); + else if (refQualKind == CXRefQualifier_RValue) + fn->setRefRef(true); + // For virtual functions, determine what it overrides + // (except for destructor for which we do not want to classify as overridden) + if (!fn->isNonvirtual() && kind != CXCursor_Destructor) + setOverridesForFunction(fn, cursor); + + Parameters ¶meters = fn->parameters(); + parameters.clear(); + parameters.reserve(function_declaration->getNumParams()); + + for (clang::ParmVarDecl* const parameter_declaration : function_declaration->parameters()) { + clang::QualType parameter_type = parameter_declaration->getOriginalType(); + + parameters.append(QString::fromStdString(get_fully_qualified_type_name( + parameter_type, + parameter_declaration->getASTContext() + ))); + + if (!parameter_type.isCanonical()) + parameters.last().setCanonicalType(QString::fromStdString(get_fully_qualified_type_name( + parameter_type.getCanonicalType(), + parameter_declaration->getASTContext() + ))); + } + + if (parameters.count() > 0) { + if (parameters.last().type().endsWith(QLatin1String("QPrivateSignal"))) { + parameters.pop_back(); // remove the QPrivateSignal argument + parameters.setPrivateSignal(); + } + } + + if (clang_isFunctionTypeVariadic(funcType)) + parameters.append(QStringLiteral("...")); + readParameterNamesAndAttributes(fn, cursor); + + if (declaration->getFriendObjectKind() != clang::Decl::FOK_None) + fn->setRelatedNonmember(true); +} + +bool ClangVisitor::parseProperty(const QString &spelling, const Location &loc) +{ + if (!spelling.startsWith(QLatin1String("Q_PROPERTY")) + && !spelling.startsWith(QLatin1String("QDOC_PROPERTY")) + && !spelling.startsWith(QLatin1String("Q_OVERRIDE"))) + return false; + + qsizetype lpIdx = spelling.indexOf(QChar('(')); + qsizetype rpIdx = spelling.lastIndexOf(QChar(')')); + if (lpIdx <= 0 || rpIdx <= lpIdx) + return false; + + QString signature = spelling.mid(lpIdx + 1, rpIdx - lpIdx - 1); + signature = signature.simplified(); + QStringList parts = signature.split(QChar(' '), Qt::SkipEmptyParts); + + static const QStringList attrs = + QStringList() << "READ" << "MEMBER" << "WRITE" + << "NOTIFY" << "CONSTANT" << "FINAL" + << "REQUIRED" << "BINDABLE" << "DESIGNABLE" + << "RESET" << "REVISION" << "SCRIPTABLE" + << "STORED" << "USER"; + + // Find the location of the first attribute. All preceding parts + // represent the property type + name. + auto it = std::find_if(parts.cbegin(), parts.cend(), + [](const QString &attr) -> bool { + return attrs.contains(attr); + }); + + if (it == parts.cend() || std::distance(parts.cbegin(), it) < 2) + return false; + + QStringList typeParts; + std::copy(parts.cbegin(), it, std::back_inserter(typeParts)); + parts.erase(parts.cbegin(), it); + QString name = typeParts.takeLast(); + + // Move the pointer operator(s) from name to type + while (!name.isEmpty() && name.front() == QChar('*')) { + typeParts.last().push_back(name.front()); + name.removeFirst(); + } + + // Need at least READ or MEMBER + getter/member name + if (parts.size() < 2 || name.isEmpty()) + return false; + + auto *property = new PropertyNode(parent_, name); + property->setAccess(Access::Public); + property->setLocation(loc); + property->setDataType(typeParts.join(QChar(' '))); + + int i = 0; + while (i < parts.size()) { + const QString &key = parts.at(i++); + // Keywords with no associated values + if (key == "CONSTANT") { + property->setConstant(); + } else if (key == "REQUIRED") { + property->setRequired(); + } + if (i < parts.size()) { + QString value = parts.at(i++); + if (key == "READ") { + qdb_->addPropertyFunction(property, value, PropertyNode::FunctionRole::Getter); + } else if (key == "WRITE") { + qdb_->addPropertyFunction(property, value, PropertyNode::FunctionRole::Setter); + property->setWritable(true); + } else if (key == "MEMBER") { + property->setWritable(true); + } else if (key == "STORED") { + property->setStored(value.toLower() == "true"); + } else if (key == "BINDABLE") { + property->setPropertyType(PropertyNode::PropertyType::BindableProperty); + qdb_->addPropertyFunction(property, value, PropertyNode::FunctionRole::Bindable); + } else if (key == "RESET") { + qdb_->addPropertyFunction(property, value, PropertyNode::FunctionRole::Resetter); + } else if (key == "NOTIFY") { + qdb_->addPropertyFunction(property, value, PropertyNode::FunctionRole::Notifier); + } + } + } + return true; +} + +/*! + Given a comment at location \a loc, return a Node for this comment + \a nextCommentLoc is the location of the next comment so the declaration + must be inbetween. + Returns nullptr if no suitable declaration was found between the two comments. + */ +Node *ClangVisitor::nodeForCommentAtLocation(CXSourceLocation loc, CXSourceLocation nextCommentLoc) +{ + ClangVisitor::SimpleLoc docloc; + clang_getPresumedLocation(loc, nullptr, &docloc.line, &docloc.column); + auto decl_it = declMap_.upperBound(docloc); + if (decl_it == declMap_.end()) + return nullptr; + + unsigned int declLine = decl_it.key().line; + unsigned int nextCommentLine; + clang_getPresumedLocation(nextCommentLoc, nullptr, &nextCommentLine, nullptr); + if (nextCommentLine < declLine) + return nullptr; // there is another comment before the declaration, ignore it. + + // make sure the previous decl was finished. + if (decl_it != declMap_.begin()) { + CXSourceLocation prevDeclEnd = clang_getRangeEnd(clang_getCursorExtent(*(std::prev(decl_it)))); + unsigned int prevDeclLine; + clang_getPresumedLocation(prevDeclEnd, nullptr, &prevDeclLine, nullptr); + if (prevDeclLine >= docloc.line) { + // The previous declaration was still going. This is only valid if the previous + // declaration is a parent of the next declaration. + auto parent = clang_getCursorLexicalParent(*decl_it); + if (!clang_equalCursors(parent, *(std::prev(decl_it)))) + return nullptr; + } + } + auto *node = findNodeForCursor(qdb_, *decl_it); + // borrow the parameter name from the definition + if (node && node->isFunction(Node::CPP)) + readParameterNamesAndAttributes(static_cast<FunctionNode *>(node), *decl_it); + return node; +} + +ClangCodeParser::ClangCodeParser( + QDocDatabase* qdb, + Config& config, + const std::vector<QByteArray>& include_paths, + const QList<QByteArray>& defines, + std::optional<std::reference_wrapper<const PCHFile>> pch +) : m_qdb{qdb}, + m_includePaths{include_paths}, + m_defines{defines}, + m_pch{pch} +{ + m_allHeaders = config.getHeaderFiles(); +} + +static const char *defaultArgs_[] = { +/* + https://bugreports.qt.io/browse/QTBUG-94365 + An unidentified bug in Clang 15.x causes parsing failures due to errors in + the AST. This replicates only with C++20 support enabled - avoid the issue + by using C++17 with Clang 15. + */ +#if LIBCLANG_VERSION_MAJOR == 15 + "-std=c++17", +#else + "-std=c++20", +#endif +#ifndef Q_OS_WIN + "-fPIC", +#else + "-fms-compatibility-version=19", +#endif + "-DQ_QDOC", + "-DQ_CLANG_QDOC", + "-DQT_DISABLE_DEPRECATED_UP_TO=0", + "-DQT_ANNOTATE_CLASS(type,...)=static_assert(sizeof(#__VA_ARGS__),#type);", + "-DQT_ANNOTATE_CLASS2(type,a1,a2)=static_assert(sizeof(#a1,#a2),#type);", + "-DQT_ANNOTATE_FUNCTION(a)=__attribute__((annotate(#a)))", + "-DQT_ANNOTATE_ACCESS_SPECIFIER(a)=__attribute__((annotate(#a)))", + "-Wno-constant-logical-operand", + "-Wno-macro-redefined", + "-Wno-nullability-completeness", + "-fvisibility=default", + "-ferror-limit=0", + ("-I" CLANG_RESOURCE_DIR) +}; + +/*! + Load the default arguments and the defines into \a args. + Clear \a args first. + */ +void getDefaultArgs(const QList<QByteArray>& defines, std::vector<const char*>& args) +{ + args.clear(); + args.insert(args.begin(), std::begin(defaultArgs_), std::end(defaultArgs_)); + + // Add the defines from the qdocconf file. + for (const auto &p : std::as_const(defines)) + args.push_back(p.constData()); +} + +static QList<QByteArray> includePathsFromHeaders(const std::set<Config::HeaderFilePath> &allHeaders) +{ + QList<QByteArray> result; + for (const auto& [header_path, _] : allHeaders) { + const QByteArray path = "-I" + header_path.toLatin1(); + const QByteArray parent = + "-I" + QDir::cleanPath(header_path + QLatin1String("/../")).toLatin1(); + } + + return result; +} + +/*! + Load the include paths into \a moreArgs. If no include paths + were provided, try to guess reasonable include paths. + */ +void getMoreArgs( + const std::vector<QByteArray>& include_paths, + const std::set<Config::HeaderFilePath>& all_headers, + std::vector<const char*>& args +) { + if (include_paths.empty()) { + /* + The include paths provided are inadequate. Make a list + of reasonable places to look for include files and use + that list instead. + */ + qCWarning(lcQdoc) << "No include paths passed to qdoc; guessing reasonable include paths"; + + QString basicIncludeDir = QDir::cleanPath(QString(Config::installDir + "/../include")); + args.emplace_back(QByteArray("-I" + basicIncludeDir.toLatin1()).constData()); + + auto include_paths_from_headers = includePathsFromHeaders(all_headers); + args.insert(args.end(), include_paths_from_headers.begin(), include_paths_from_headers.end()); + } else { + std::copy(include_paths.begin(), include_paths.end(), std::back_inserter(args)); + } +} + +/*! + Building the PCH must be possible when there are no .cpp + files, so it is moved here to its own member function, and + it is called after the list of header files is complete. + */ +std::optional<PCHFile> buildPCH( + QDocDatabase* qdb, + QString module_header, + const std::set<Config::HeaderFilePath>& all_headers, + const std::vector<QByteArray>& include_paths, + const QList<QByteArray>& defines +) { + static std::vector<const char*> arguments{}; + + if (module_header.isEmpty()) return std::nullopt; + + getDefaultArgs(defines, arguments); + getMoreArgs(include_paths, all_headers, arguments); + + flags_ = static_cast<CXTranslationUnit_Flags>(CXTranslationUnit_Incomplete + | CXTranslationUnit_SkipFunctionBodies + | CXTranslationUnit_KeepGoing); + + CompilationIndex index{ clang_createIndex(1, kClangDontDisplayDiagnostics) }; + + QTemporaryDir pch_directory{QDir::tempPath() + QLatin1String("/qdoc_pch")}; + if (!pch_directory.isValid()) return std::nullopt; + + const QByteArray module = module_header.toUtf8(); + QByteArray header; + + qCDebug(lcQdoc) << "Build and visit PCH for" << module_header; + // A predicate for std::find_if() to locate a path to the module's header + // (e.g. QtGui/QtGui) to be used as pre-compiled header + struct FindPredicate + { + enum SearchType { Any, Module }; + QByteArray &candidate_; + const QByteArray &module_; + SearchType type_; + FindPredicate(QByteArray &candidate, const QByteArray &module, + SearchType type = Any) + : candidate_(candidate), module_(module), type_(type) + { + } + + bool operator()(const QByteArray &p) const + { + if (type_ != Any && !p.endsWith(module_)) + return false; + candidate_ = p + "/"; + candidate_.append(module_); + if (p.startsWith("-I")) + candidate_ = candidate_.mid(2); + return QFile::exists(QString::fromUtf8(candidate_)); + } + }; + + // First, search for an include path that contains the module name, then any path + QByteArray candidate; + auto it = std::find_if(include_paths.begin(), include_paths.end(), + FindPredicate(candidate, module, FindPredicate::Module)); + if (it == include_paths.end()) + it = std::find_if(include_paths.begin(), include_paths.end(), + FindPredicate(candidate, module, FindPredicate::Any)); + if (it != include_paths.end()) + header = candidate; + + if (header.isEmpty()) { + qWarning() << "(qdoc) Could not find the module header in include paths for module" + << module << " (include paths: " << include_paths << ")"; + qWarning() << " Artificial module header built from header dirs in qdocconf " + "file"; + } + arguments.push_back("-xc++"); + + TranslationUnit tu; + + QString tmpHeader = pch_directory.path() + "/" + module; + if (QFile tmpHeaderFile(tmpHeader); tmpHeaderFile.open(QIODevice::Text | QIODevice::WriteOnly)) { + QTextStream out(&tmpHeaderFile); + if (header.isEmpty()) { + for (const auto& [header_path, header_name] : all_headers) { + if (!header_name.endsWith(QLatin1String("_p.h")) + && !header_name.startsWith(QLatin1String("moc_"))) { + QString line = QLatin1String("#include \"") + header_path + + QLatin1String("/") + header_name + QLatin1String("\""); + out << line << "\n"; + + } + } + } else { + QFileInfo headerFile(header); + if (!headerFile.exists()) { + qWarning() << "Could not find module header file" << header; + return std::nullopt; + } + out << QLatin1String("#include \"") + header + QLatin1String("\""); + } + } + + CXErrorCode err = + clang_parseTranslationUnit2(index, tmpHeader.toLatin1().data(), arguments.data(), + static_cast<int>(arguments.size()), nullptr, 0, + flags_ | CXTranslationUnit_ForSerialization, &tu.tu); + qCDebug(lcQdoc) << __FUNCTION__ << "clang_parseTranslationUnit2(" << tmpHeader << arguments + << ") returns" << err; + + printDiagnostics(tu); + + if (err || !tu) { + qCCritical(lcQdoc) << "Could not create PCH file for " << module_header; + return std::nullopt; + } + + QByteArray pch_name = pch_directory.path().toUtf8() + "/" + module + ".pch"; + auto error = clang_saveTranslationUnit(tu, pch_name.constData(), + clang_defaultSaveOptions(tu)); + if (error) { + qCCritical(lcQdoc) << "Could not save PCH file for" << module_header; + return std::nullopt; + } + + // Visit the header now, as token from pre-compiled header won't be visited + // later + CXCursor cur = clang_getTranslationUnitCursor(tu); + ClangVisitor visitor(qdb, all_headers); + visitor.visitChildren(cur); + qCDebug(lcQdoc) << "PCH built and visited for" << module_header; + + return std::make_optional(PCHFile{std::move(pch_directory), pch_name}); +} + +static float getUnpatchedVersion(QString t) +{ + if (t.count(QChar('.')) > 1) + t.truncate(t.lastIndexOf(QChar('.'))); + return t.toFloat(); +} + +/*! + Get ready to parse the C++ cpp file identified by \a filePath + and add its parsed contents to the database. \a location is + used for reporting errors. + + Call matchDocsAndStuff() to do all the parsing and tree building. + */ +ParsedCppFileIR ClangCodeParser::parse_cpp_file(const QString &filePath) +{ + flags_ = static_cast<CXTranslationUnit_Flags>(CXTranslationUnit_Incomplete + | CXTranslationUnit_SkipFunctionBodies + | CXTranslationUnit_KeepGoing); + + CompilationIndex index{ clang_createIndex(1, kClangDontDisplayDiagnostics) }; + + getDefaultArgs(m_defines, m_args); + if (m_pch && !filePath.endsWith(".mm")) { + m_args.push_back("-w"); + m_args.push_back("-include-pch"); + m_args.push_back((*m_pch).get().name.constData()); + } + getMoreArgs(m_includePaths, m_allHeaders, m_args); + + TranslationUnit tu; + CXErrorCode err = + clang_parseTranslationUnit2(index, filePath.toLocal8Bit(), m_args.data(), + static_cast<int>(m_args.size()), nullptr, 0, flags_, &tu.tu); + qCDebug(lcQdoc) << __FUNCTION__ << "clang_parseTranslationUnit2(" << filePath << m_args + << ") returns" << err; + printDiagnostics(tu); + + if (err || !tu) { + qWarning() << "(qdoc) Could not parse source file" << filePath << " error code:" << err; + return {}; + } + + ParsedCppFileIR parse_result{}; + + CXCursor tuCur = clang_getTranslationUnitCursor(tu); + ClangVisitor visitor(m_qdb, m_allHeaders); + visitor.visitChildren(tuCur); + + CXToken *tokens; + unsigned int numTokens = 0; + const QSet<QString> &commands = CppCodeParser::topic_commands + CppCodeParser::meta_commands; + clang_tokenize(tu, clang_getCursorExtent(tuCur), &tokens, &numTokens); + + for (unsigned int i = 0; i < numTokens; ++i) { + if (clang_getTokenKind(tokens[i]) != CXToken_Comment) + continue; + QString comment = fromCXString(clang_getTokenSpelling(tu, tokens[i])); + if (!comment.startsWith("/*!")) + continue; + + auto commentLoc = clang_getTokenLocation(tu, tokens[i]); + auto loc = fromCXSourceLocation(commentLoc); + auto end_loc = fromCXSourceLocation(clang_getRangeEnd(clang_getTokenExtent(tu, tokens[i]))); + Doc::trimCStyleComment(loc, comment); + + // Doc constructor parses the comment. + Doc doc(loc, end_loc, comment, commands, CppCodeParser::topic_commands); + if (hasTooManyTopics(doc)) + continue; + + if (doc.topicsUsed().isEmpty()) { + Node *n = nullptr; + if (i + 1 < numTokens) { + // Try to find the next declaration. + CXSourceLocation nextCommentLoc = commentLoc; + while (i + 2 < numTokens && clang_getTokenKind(tokens[i + 1]) != CXToken_Comment) + ++i; // already skip all the tokens that are not comments + nextCommentLoc = clang_getTokenLocation(tu, tokens[i + 1]); + n = visitor.nodeForCommentAtLocation(commentLoc, nextCommentLoc); + } + + if (n) { + parse_result.tied.emplace_back(TiedDocumentation{doc, n}); + } else if (CodeParser::isWorthWarningAbout(doc)) { + bool future = false; + if (doc.metaCommandsUsed().contains(COMMAND_SINCE)) { + QString sinceVersion = doc.metaCommandArgs(COMMAND_SINCE).at(0).first; + if (getUnpatchedVersion(sinceVersion) > + getUnpatchedVersion(Config::instance().get(CONFIG_VERSION).asString())) + future = true; + } + if (!future) { + doc.location().warning( + QStringLiteral("Cannot tie this documentation to anything"), + QStringLiteral("qdoc found a /*! ... */ comment, but there was no " + "topic command (e.g., '\\%1', '\\%2') in the " + "comment and no function definition following " + "the comment.") + .arg(COMMAND_FN, COMMAND_PAGE)); + } + } + } else { + parse_result.untied.emplace_back(UntiedDocumentation{doc, QStringList()}); + + CXCursor cur = clang_getCursor(tu, commentLoc); + while (true) { + CXCursorKind kind = clang_getCursorKind(cur); + if (clang_isTranslationUnit(kind) || clang_isInvalid(kind)) + break; + if (kind == CXCursor_Namespace) { + parse_result.untied.back().context << fromCXString(clang_getCursorSpelling(cur)); + } + cur = clang_getCursorLexicalParent(cur); + } + } + } + + clang_disposeTokens(tu, tokens, numTokens); + m_namespaceScope.clear(); + s_fn.clear(); + + return parse_result; +} + +/*! + Use clang to parse the function signature from a function + command. \a location is used for reporting errors. \a fnSignature + is the string to parse. It is always a function decl. + \a idTag is the optional bracketed argument passed to \\fn, or + an empty string. + \a context is a string list representing the scope (namespaces) + under which the function is declared. + + Returns a variant that's either a Node instance tied to the + function declaration, or a parsing failure for later processing. + */ +std::variant<Node*, FnMatchError> FnCommandParser::operator()(const Location &location, const QString &fnSignature, + const QString &idTag, QStringList context) +{ + Node *fnNode = nullptr; + /* + If the \fn command begins with a tag, then don't try to + parse the \fn command with clang. Use the tag to search + for the correct function node. It is an error if it can + not be found. Return 0 in that case. + */ + if (!idTag.isEmpty()) { + fnNode = m_qdb->findFunctionNodeForTag(idTag); + if (!fnNode) { + location.error( + QStringLiteral("tag \\fn [%1] not used in any include file in current module").arg(idTag)); + } else { + /* + The function node was found. Use the formal + parameter names from the \fn command, because + they will be the names used in the documentation. + */ + auto *fn = static_cast<FunctionNode *>(fnNode); + QStringList leftParenSplit = fnSignature.mid(fnSignature.indexOf(fn->name())).split('('); + if (leftParenSplit.size() > 1) { + QStringList rightParenSplit = leftParenSplit[1].split(')'); + if (!rightParenSplit.empty()) { + QString params = rightParenSplit[0]; + if (!params.isEmpty()) { + QStringList commaSplit = params.split(','); + Parameters ¶meters = fn->parameters(); + if (parameters.count() == commaSplit.size()) { + for (int i = 0; i < parameters.count(); ++i) { + QStringList blankSplit = commaSplit[i].split(' ', Qt::SkipEmptyParts); + if (blankSplit.size() > 1) { + QString pName = blankSplit.last(); + // Remove any non-letters from the start of parameter name + auto it = std::find_if(std::begin(pName), std::end(pName), + [](const QChar &c) { return c.isLetter(); }); + parameters[i].setName( + pName.remove(0, std::distance(std::begin(pName), it))); + } + } + } + } + } + } + } + return fnNode; + } + auto flags = static_cast<CXTranslationUnit_Flags>(CXTranslationUnit_Incomplete + | CXTranslationUnit_SkipFunctionBodies + | CXTranslationUnit_KeepGoing); + + CompilationIndex index{ clang_createIndex(1, kClangDontDisplayDiagnostics) }; + + getDefaultArgs(m_defines, m_args); + + if (m_pch) { + m_args.push_back("-w"); + m_args.push_back("-include-pch"); + m_args.push_back((*m_pch).get().name.constData()); + } + + TranslationUnit tu; + QByteArray s_fn{}; + for (const auto &ns : std::as_const(context)) + s_fn.prepend("namespace " + ns.toUtf8() + " {"); + s_fn += fnSignature.toUtf8(); + if (!s_fn.endsWith(";")) + s_fn += "{ }"; + s_fn.append(context.size(), '}'); + + const char *dummyFileName = fnDummyFileName; + CXUnsavedFile unsavedFile { dummyFileName, s_fn.constData(), + static_cast<unsigned long>(s_fn.size()) }; + CXErrorCode err = clang_parseTranslationUnit2(index, dummyFileName, m_args.data(), + int(m_args.size()), &unsavedFile, 1, flags, &tu.tu); + qCDebug(lcQdoc) << __FUNCTION__ << "clang_parseTranslationUnit2(" << dummyFileName << m_args + << ") returns" << err; + printDiagnostics(tu); + if (err || !tu) { + location.error(QStringLiteral("clang could not parse \\fn %1").arg(fnSignature)); + return fnNode; + } else { + /* + Always visit the tu if one is constructed, because + it might be possible to find the correct node, even + if clang detected diagnostics. Only bother to report + the diagnostics if they stop us finding the node. + */ + CXCursor cur = clang_getTranslationUnitCursor(tu); + ClangVisitor visitor(m_qdb, m_allHeaders); + bool ignoreSignature = false; + visitor.visitFnArg(cur, &fnNode, ignoreSignature); + + if (!fnNode) { + unsigned diagnosticCount = clang_getNumDiagnostics(tu); + const auto &config = Config::instance(); + if (diagnosticCount > 0 && (!config.preparing() || config.singleExec())) { + return FnMatchError{ fnSignature, location }; + } + } + } + return fnNode; +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/clangcodeparser.h b/src/qdoc/qdoc/src/qdoc/clangcodeparser.h new file mode 100644 index 000000000..d7568f9a4 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/clangcodeparser.h @@ -0,0 +1,94 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef CLANGCODEPARSER_H +#define CLANGCODEPARSER_H + +#include "codeparser.h" +#include "parsererror.h" +#include "config.h" + +#include <QtCore/qtemporarydir.h> +#include <QtCore/QStringList> + +#include <optional> + +typedef struct CXTranslationUnitImpl *CXTranslationUnit; + +class CppCodeParser; + +QT_BEGIN_NAMESPACE + +struct ParsedCppFileIR { + std::vector<UntiedDocumentation> untied; + std::vector<TiedDocumentation> tied; +}; + +struct PCHFile { + QTemporaryDir dir; + QByteArray name; +}; + +std::optional<PCHFile> buildPCH( + QDocDatabase* qdb, + QString module_header, + const std::set<Config::HeaderFilePath>& all_headers, + const std::vector<QByteArray>& include_paths, + const QList<QByteArray>& defines +); + +struct FnCommandParser { + FnCommandParser( + QDocDatabase* qdb, + const std::set<Config::HeaderFilePath>& all_headers, + const QList<QByteArray>& defines, + std::optional<std::reference_wrapper<const PCHFile>> pch + ) : m_qdb{qdb}, + m_allHeaders{all_headers}, + m_defines{defines}, + m_args{}, + m_pch{pch} + {} + + std::variant<Node*, FnMatchError> operator()( + const Location &location, + const QString &fnSignature, + const QString &idTag, + QStringList context + ); + +private: + QDocDatabase* m_qdb; + const std::set<Config::HeaderFilePath>& m_allHeaders; // file name->path + QList<QByteArray> m_defines {}; + std::vector<const char *> m_args {}; + std::optional<std::reference_wrapper<const PCHFile>> m_pch; +}; + +class ClangCodeParser +{ +public: + ClangCodeParser( + QDocDatabase* qdb, + Config&, + const std::vector<QByteArray>& include_paths, + const QList<QByteArray>& defines, + std::optional<std::reference_wrapper<const PCHFile>> pch + ); + + ParsedCppFileIR parse_cpp_file(const QString &filePath); + +private: + QDocDatabase* m_qdb{}; + std::set<Config::HeaderFilePath> m_allHeaders {}; // file name->path + const std::vector<QByteArray>& m_includePaths; + QList<QByteArray> m_defines {}; + std::vector<const char *> m_args {}; + QStringList m_namespaceScope {}; + QByteArray s_fn; + std::optional<std::reference_wrapper<const PCHFile>> m_pch; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/classnode.cpp b/src/qdoc/qdoc/src/qdoc/classnode.cpp new file mode 100644 index 000000000..1b132f91e --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/classnode.cpp @@ -0,0 +1,260 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "classnode.h" + +#include "functionnode.h" +#include "propertynode.h" +#include "qdocdatabase.h" +#include "qmltypenode.h" + +QT_BEGIN_NAMESPACE + +/*! + \class ClassNode + \brief The ClassNode represents a C++ class. + + It is also used to represent a C++ struct or union. There are some + actual uses for structs, but I don't think any unions have been + documented yet. + */ + +/*! + Adds the base class \a node to this class's list of base + classes. The base class has the specified \a access. This + is a resolved base class. + */ +void ClassNode::addResolvedBaseClass(Access access, ClassNode *node) +{ + m_bases.append(RelatedClass(access, node)); + node->m_derived.append(RelatedClass(access, this)); +} + +/*! + Adds the derived class \a node to this class's list of derived + classes. The derived class inherits this class with \a access. + */ +void ClassNode::addDerivedClass(Access access, ClassNode *node) +{ + m_derived.append(RelatedClass(access, node)); +} + +/*! + Add an unresolved base class to this class node's list of + base classes. The unresolved base class will be resolved + before the generate phase of qdoc. In an unresolved base + class, the pointer to the base class node is 0. + */ +void ClassNode::addUnresolvedBaseClass(Access access, const QStringList &path) +{ + m_bases.append(RelatedClass(access, path)); +} + +/*! + Search the child list to find the property node with the + specified \a name. + */ +PropertyNode *ClassNode::findPropertyNode(const QString &name) +{ + Node *n = findNonfunctionChild(name, &Node::isProperty); + + if (n) + return static_cast<PropertyNode *>(n); + + PropertyNode *pn = nullptr; + + const QList<RelatedClass> &bases = baseClasses(); + if (!bases.isEmpty()) { + for (const RelatedClass &base : bases) { + ClassNode *cn = base.m_node; + if (cn) { + pn = cn->findPropertyNode(name); + if (pn) + break; + } + } + } + const QList<RelatedClass> &ignoredBases = ignoredBaseClasses(); + if (!ignoredBases.isEmpty()) { + for (const RelatedClass &base : ignoredBases) { + ClassNode *cn = base.m_node; + if (cn) { + pn = cn->findPropertyNode(name); + if (pn) + break; + } + } + } + + return pn; +} + +/*! + \a fn is an overriding function in this class or in a class + derived from this class. Find the node for the function that + \a fn overrides in this class's children or in one of this + class's base classes. Return a pointer to the overridden + function or return 0. + + This should be revised because clang provides the path to the + overridden function. mws 15/12/2018 + */ +FunctionNode *ClassNode::findOverriddenFunction(const FunctionNode *fn) +{ + for (auto &bc : m_bases) { + ClassNode *cn = bc.m_node; + if (cn == nullptr) { + cn = QDocDatabase::qdocDB()->findClassNode(bc.m_path); + bc.m_node = cn; + } + if (cn != nullptr) { + FunctionNode *result = cn->findFunctionChild(fn); + if (result != nullptr && !result->isInternal() && !result->isNonvirtual() + && result->hasDoc()) + return result; + result = cn->findOverriddenFunction(fn); + if (result != nullptr && !result->isNonvirtual()) + return result; + } + } + return nullptr; +} + +/*! + \a fn is an overriding function in this class or in a class + derived from this class. Find the node for the property that + \a fn overrides in this class's children or in one of this + class's base classes. Return a pointer to the overridden + property or return 0. + */ +PropertyNode *ClassNode::findOverriddenProperty(const FunctionNode *fn) +{ + for (auto &baseClass : m_bases) { + ClassNode *cn = baseClass.m_node; + if (cn == nullptr) { + cn = QDocDatabase::qdocDB()->findClassNode(baseClass.m_path); + baseClass.m_node = cn; + } + if (cn != nullptr) { + const NodeList &children = cn->childNodes(); + for (const auto &child : children) { + if (child->isProperty()) { + auto *pn = static_cast<PropertyNode *>(child); + if (pn->name() == fn->name() || pn->hasAccessFunction(fn->name())) { + if (pn->hasDoc()) + return pn; + } + } + } + PropertyNode *result = cn->findOverriddenProperty(fn); + if (result != nullptr) + return result; + } + } + return nullptr; +} + +/*! + Returns true if the class or struct represented by this class + node must be documented. If this function returns true, then + qdoc must find a qdoc comment for this class. If it returns + false, then the class need not be documented. + */ +bool ClassNode::docMustBeGenerated() const +{ + if (!hasDoc() || isPrivate() || isInternal() || isDontDocument()) + return false; + if (declLocation().fileName().endsWith(QLatin1String("_p.h")) && !hasDoc()) + return false; + + return true; +} + +/*! + A base class of this class node was private or internal. + That node's list of \a bases is traversed in this function. + Each of its public base classes is promoted to be a base + class of this node for documentation purposes. For each + private or internal class node in \a bases, this function + is called recursively with the list of base classes from + that private or internal class node. + */ +void ClassNode::promotePublicBases(const QList<RelatedClass> &bases) +{ + if (!bases.isEmpty()) { + for (qsizetype i = bases.size() - 1; i >= 0; --i) { + ClassNode *bc = bases.at(i).m_node; + if (bc == nullptr) + bc = QDocDatabase::qdocDB()->findClassNode(bases.at(i).m_path); + if (bc != nullptr) { + if (bc->isPrivate() || bc->isInternal()) + promotePublicBases(bc->baseClasses()); + else + m_bases.append(bases.at(i)); + } + } + } +} + +/*! + Remove private and internal bases classes from this class's list + of base classes. When a base class is removed from the list, add + its base classes to this class's list of base classes. + */ +void ClassNode::removePrivateAndInternalBases() +{ + int i; + i = 0; + QSet<ClassNode *> found; + + // Remove private and duplicate base classes. + while (i < m_bases.size()) { + ClassNode *bc = m_bases.at(i).m_node; + if (bc == nullptr) + bc = QDocDatabase::qdocDB()->findClassNode(m_bases.at(i).m_path); + if (bc != nullptr + && (bc->isPrivate() || bc->isInternal() || bc->isDontDocument() + || found.contains(bc))) { + RelatedClass rc = m_bases.at(i); + m_bases.removeAt(i); + m_ignoredBases.append(rc); + promotePublicBases(bc->baseClasses()); + } else { + ++i; + } + found.insert(bc); + } + + i = 0; + while (i < m_derived.size()) { + ClassNode *dc = m_derived.at(i).m_node; + if (dc != nullptr && (dc->isPrivate() || dc->isInternal() || dc->isDontDocument())) { + m_derived.removeAt(i); + const QList<RelatedClass> &dd = dc->derivedClasses(); + for (qsizetype j = dd.size() - 1; j >= 0; --j) + m_derived.insert(i, dd.at(j)); + } else { + ++i; + } + } +} + +/*! + */ +void ClassNode::resolvePropertyOverriddenFromPtrs(PropertyNode *pn) +{ + for (const auto &baseClass : std::as_const(baseClasses())) { + ClassNode *cn = baseClass.m_node; + if (cn) { + Node *n = cn->findNonfunctionChild(pn->name(), &Node::isProperty); + if (n) { + auto *baseProperty = static_cast<PropertyNode *>(n); + cn->resolvePropertyOverriddenFromPtrs(baseProperty); + pn->setOverriddenFrom(baseProperty); + } else + cn->resolvePropertyOverriddenFromPtrs(pn); + } + } +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/classnode.h b/src/qdoc/qdoc/src/qdoc/classnode.h new file mode 100644 index 000000000..1ac944a34 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/classnode.h @@ -0,0 +1,69 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef CLASSNODE_H +#define CLASSNODE_H + +#include "aggregate.h" +#include "relatedclass.h" + +#include <QtCore/qglobal.h> +#include <QtCore/qlist.h> +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +class FunctionNode; +class PropertyNode; +class QmlTypeNode; + +class ClassNode : public Aggregate +{ +public: + ClassNode(NodeType type, Aggregate *parent, const QString &name) : Aggregate(type, parent, name) + { + } + [[nodiscard]] bool isFirstClassAggregate() const override { return true; } + [[nodiscard]] bool isClassNode() const override { return true; } + [[nodiscard]] bool isRelatableType() const override { return true; } + [[nodiscard]] bool isWrapper() const override { return m_wrapper; } + void setWrapper() override { m_wrapper = true; } + + void addResolvedBaseClass(Access access, ClassNode *node); + void addDerivedClass(Access access, ClassNode *node); + void addUnresolvedBaseClass(Access access, const QStringList &path); + void removePrivateAndInternalBases(); + void resolvePropertyOverriddenFromPtrs(PropertyNode *pn); + + QList<RelatedClass> &baseClasses() { return m_bases; } + QList<RelatedClass> &derivedClasses() { return m_derived; } + QList<RelatedClass> &ignoredBaseClasses() { return m_ignoredBases; } + + [[nodiscard]] const QList<RelatedClass> &baseClasses() const { return m_bases; } + + [[nodiscard]] bool isAbstract() const override { return m_abstract; } + void setAbstract(bool b) override { m_abstract = b; } + PropertyNode *findPropertyNode(const QString &name); + FunctionNode *findOverriddenFunction(const FunctionNode *fn); + PropertyNode *findOverriddenProperty(const FunctionNode *fn); + [[nodiscard]] bool docMustBeGenerated() const override; + + void insertQmlNativeType(QmlTypeNode *qmlTypeNode) { m_nativeTypeForQml << qmlTypeNode; } + bool isQmlNativeType() { return !m_nativeTypeForQml.empty(); } + const QSet<QmlTypeNode *> &qmlNativeTypes() { return m_nativeTypeForQml; } + +private: + void promotePublicBases(const QList<RelatedClass> &bases); + +private: + QList<RelatedClass> m_bases {}; + QList<RelatedClass> m_derived {}; + QList<RelatedClass> m_ignoredBases {}; + bool m_abstract { false }; + bool m_wrapper { false }; + QSet<QmlTypeNode *> m_nativeTypeForQml; +}; + +QT_END_NAMESPACE + +#endif // CLASSNODE_H diff --git a/src/qdoc/qdoc/src/qdoc/codechunk.cpp b/src/qdoc/qdoc/src/qdoc/codechunk.cpp new file mode 100644 index 000000000..889e091af --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/codechunk.cpp @@ -0,0 +1,104 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "codechunk.h" + +QT_BEGIN_NAMESPACE + +enum { Other, Alnum, Gizmo, Comma, LBrace, RBrace, RAngle, Colon, Paren }; + +// entries 128 and above are Other +static const int charCategory[256] = { Other, Other, Other, Other, Other, Other, Other, Other, + Other, Other, Other, Other, Other, Other, Other, Other, + Other, Other, Other, Other, Other, Other, Other, Other, + Other, Other, Other, Other, Other, Other, Other, Other, + // ! " # $ % & ' + Other, Other, Other, Other, Other, Gizmo, Gizmo, Other, + // ( ) * + , - . / + Paren, Paren, Gizmo, Gizmo, Comma, Other, Other, Gizmo, + // 0 1 2 3 4 5 6 7 + Alnum, Alnum, Alnum, Alnum, Alnum, Alnum, Alnum, Alnum, + // 8 9 : ; < = > ? + Alnum, Alnum, Colon, Other, Other, Gizmo, RAngle, Gizmo, + // @ A B C D E F G + Other, Alnum, Alnum, Alnum, Alnum, Alnum, Alnum, Alnum, + // H I J K L M N O + Alnum, Alnum, Alnum, Alnum, Alnum, Alnum, Alnum, Alnum, + // P Q R S T U V W + Alnum, Alnum, Alnum, Alnum, Alnum, Alnum, Alnum, Alnum, + // X Y Z [ \ ] ^ _ + Alnum, Alnum, Alnum, Other, Other, Other, Gizmo, Alnum, + // ` a b c d e f g + Other, Alnum, Alnum, Alnum, Alnum, Alnum, Alnum, Alnum, + // h i j k l m n o + Alnum, Alnum, Alnum, Alnum, Alnum, Alnum, Alnum, Alnum, + // p q r s t u v w + Alnum, Alnum, Alnum, Alnum, Alnum, Alnum, Alnum, Alnum, + // x y z { | } ~ + Alnum, Alnum, Alnum, LBrace, Gizmo, RBrace, Other, Other }; + +static const bool needSpace[9][9] = { + /* [ a + , { } > : ) */ + /* [ */ { false, false, false, false, false, true, false, false, false }, + /* a */ { false, true, true, false, false, true, false, false, false }, + /* + */ { false, true, false, false, false, true, false, true, false }, + /* , */ { true, true, true, true, true, true, true, true, false }, + /* { */ { false, false, false, false, false, false, false, false, false }, + /* } */ { false, false, false, false, false, false, false, false, false }, + /* > */ { true, true, true, false, true, true, true, false, false }, + /* : */ { false, false, true, true, true, true, true, false, false }, + /* ( */ { false, false, false, false, false, false, false, false, false }, +}; + +static int category(QChar ch) +{ + return charCategory[static_cast<int>(ch.toLatin1())]; +} + +/*! + \class CodeChunk + + \brief The CodeChunk class represents a tiny piece of C++ code. + + \note I think this class should be eliminated (mws 11/12/2018 + + The class provides conversion between a list of lexemes and a string. It adds + spaces at the right place for consistent style. The tiny pieces of code it + represents are data types, enum values, and default parameter values. + + Apart from the piece of code itself, there are two bits of metainformation + stored in CodeChunk: the base and the hotspot. The base is the part of the + piece that may be a hypertext link. The base of + + QMap<QString, QString> + + is QMap. + + The hotspot is the place the variable name should be inserted in the case of a + variable (or parameter) declaration. The hotspot of + + char * [] + + is between '*' and '[]'. +*/ + +/*! + Appends \a lexeme to the current string contents, inserting + a space if appropriate. + */ +void CodeChunk::append(const QString &lexeme) +{ + if (!m_str.isEmpty() && !lexeme.isEmpty()) { + /* + Should there be a space or not between the code chunk so far and the + new lexeme? + */ + int cat1 = category(m_str.at(m_str.size() - 1)); + int cat2 = category(lexeme[0]); + if (needSpace[cat1][cat2]) + m_str += QLatin1Char(' '); + } + m_str += lexeme; +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/codechunk.h b/src/qdoc/qdoc/src/qdoc/codechunk.h new file mode 100644 index 000000000..00ad26c6d --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/codechunk.h @@ -0,0 +1,74 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef CODECHUNK_H +#define CODECHUNK_H + +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +// ### get rid of that class + +class CodeChunk +{ +public: + CodeChunk() : m_hotspot(-1) { } + + void append(const QString &lexeme); + void appendHotspot() + { + if (m_hotspot == -1) + m_hotspot = m_str.size(); + } + + [[nodiscard]] bool isEmpty() const { return m_str.isEmpty(); } + void clear() { m_str.clear(); } + [[nodiscard]] QString toString() const { return m_str; } + [[nodiscard]] QString left() const + { + return m_str.left(m_hotspot == -1 ? m_str.size() : m_hotspot); + } + [[nodiscard]] QString right() const + { + return m_str.mid(m_hotspot == -1 ? m_str.size() : m_hotspot); + } + +private: + QString m_str {}; + qsizetype m_hotspot {}; +}; + +inline bool operator==(const CodeChunk &c, const CodeChunk &d) +{ + return c.toString() == d.toString(); +} + +inline bool operator!=(const CodeChunk &c, const CodeChunk &d) +{ + return !(c == d); +} + +inline bool operator<(const CodeChunk &c, const CodeChunk &d) +{ + return c.toString() < d.toString(); +} + +inline bool operator>(const CodeChunk &c, const CodeChunk &d) +{ + return d < c; +} + +inline bool operator<=(const CodeChunk &c, const CodeChunk &d) +{ + return !(c > d); +} + +inline bool operator>=(const CodeChunk &c, const CodeChunk &d) +{ + return !(c < d); +} + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/codemarker.cpp b/src/qdoc/qdoc/src/qdoc/codemarker.cpp new file mode 100644 index 000000000..28f84a946 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/codemarker.cpp @@ -0,0 +1,448 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "codemarker.h" + +#include "classnode.h" +#include "config.h" +#include "functionnode.h" +#include "node.h" +#include "propertynode.h" +#include "qmlpropertynode.h" + +#include <QtCore/qobjectdefs.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +QString CodeMarker::s_defaultLang; +QList<CodeMarker *> CodeMarker::s_markers; + + +/*! + When a code marker constructs itself, it puts itself into + the static list of code markers. All the code markers in + the static list get initialized in initialize(), which is + not called until after the qdoc configuration file has + been read. + */ +CodeMarker::CodeMarker() +{ + s_markers.prepend(this); +} + +/*! + When a code marker destroys itself, it removes itself from + the static list of code markers. + */ +CodeMarker::~CodeMarker() +{ + s_markers.removeAll(this); +} + +/*! + A code market performs no initialization by default. Marker-specific + initialization is performed in subclasses. + */ +void CodeMarker::initializeMarker() {} + +/*! + Terminating a code marker is trivial. + */ +void CodeMarker::terminateMarker() +{ + // nothing. +} + +/*! + All the code markers in the static list are initialized + here, after the qdoc configuration file has been loaded. + */ +void CodeMarker::initialize() +{ + s_defaultLang = Config::instance().get(CONFIG_LANGUAGE).asString(); + for (const auto &marker : std::as_const(s_markers)) + marker->initializeMarker(); +} + +/*! + All the code markers in the static list are terminated here. + */ +void CodeMarker::terminate() +{ + for (const auto &marker : std::as_const(s_markers)) + marker->terminateMarker(); +} + +CodeMarker *CodeMarker::markerForCode(const QString &code) +{ + CodeMarker *defaultMarker = markerForLanguage(s_defaultLang); + if (defaultMarker != nullptr && defaultMarker->recognizeCode(code)) + return defaultMarker; + + for (const auto &marker : std::as_const(s_markers)) { + if (marker->recognizeCode(code)) + return marker; + } + + return defaultMarker; +} + +CodeMarker *CodeMarker::markerForFileName(const QString &fileName) +{ + CodeMarker *defaultMarker = markerForLanguage(s_defaultLang); + qsizetype dot = -1; + while ((dot = fileName.lastIndexOf(QLatin1Char('.'), dot)) != -1) { + QString ext = fileName.mid(dot + 1); + if (defaultMarker != nullptr && defaultMarker->recognizeExtension(ext)) + return defaultMarker; + for (const auto &marker : std::as_const(s_markers)) { + if (marker->recognizeExtension(ext)) + return marker; + } + --dot; + } + return defaultMarker; +} + +CodeMarker *CodeMarker::markerForLanguage(const QString &lang) +{ + for (const auto &marker : std::as_const(s_markers)) { + if (marker->recognizeLanguage(lang)) + return marker; + } + return nullptr; +} + +const Node *CodeMarker::nodeForString(const QString &string) +{ +#if QT_POINTER_SIZE == 4 + const quintptr n = string.toUInt(); +#else + const quintptr n = string.toULongLong(); +#endif + return reinterpret_cast<const Node *>(n); +} + +QString CodeMarker::stringForNode(const Node *node) +{ + return QString::number(reinterpret_cast<quintptr>(node)); +} + +/*! + Returns a string representing the \a node status, set using \preliminary, \since, + and \deprecated commands. + + If a string is returned, it is one of: + \list + \li \c {"preliminary"} + \li \c {"since <version_since>, deprecated in <version_deprecated>"} + \li \c {"since <version_since>, until <version_deprecated>"} + \li \c {"since <version_since>"} + \li \c {"since <version_since>, deprecated"} + \li \c {"deprecated in <version_deprecated>"} + \li \c {"until <version_deprecated>"} + \li \c {"deprecated"} + \endlist + + If \a node has no related status information, returns std::nullopt. +*/ +static std::optional<QString> nodeStatusAsString(const Node *node) +{ + if (node->isPreliminary()) + return std::optional(u"preliminary"_s); + + QStringList result; + if (const auto &since = node->since(); !since.isEmpty()) + result << "since %1"_L1.arg(since); + if (const auto &deprecated = node->deprecatedSince(); !deprecated.isEmpty()) { + if (node->isDeprecated()) + result << "deprecated in %1"_L1.arg(deprecated); + else + result << "until %1"_L1.arg(deprecated); + } else if (node->isDeprecated()) { + result << u"deprecated"_s; + } + + return result.isEmpty() ? std::nullopt : std::optional(result.join(u", "_s)); +} + +/*! + Returns the 'extra' synopsis string for \a node with status information, + using a specified section \a style. +*/ +QString CodeMarker::extraSynopsis(const Node *node, Section::Style style) +{ + if (style != Section::Summary && style != Section::Details) + return {}; + + QStringList extra; + if (style == Section::Details) { + switch (node->nodeType()) { + case Node::Function: { + const auto *func = static_cast<const FunctionNode *>(node); + if (func->isStatic()) { + extra << "static"; + } else if (!func->isNonvirtual()) { + if (func->isFinal()) + extra << "final"; + if (func->isOverride()) + extra << "override"; + if (func->isPureVirtual()) + extra << "pure"; + extra << "virtual"; + } + + if (func->isExplicit()) extra << "explicit"; + if (func->isConstexpr()) extra << "constexpr"; + if (auto noexcept_info = func->getNoexcept()) { + extra << (QString("noexcept") + (!(*noexcept_info).isEmpty() ? "(...)" : "")); + } + + if (func->access() == Access::Protected) + extra << "protected"; + else if (func->access() == Access::Private) + extra << "private"; + + if (func->isSignal()) { + if (func->parameters().isPrivateSignal()) + extra << "private"; + extra << "signal"; + } else if (func->isSlot()) + extra << "slot"; + else if (func->isDefault()) + extra << "default"; + else if (func->isInvokable()) + extra << "invokable"; + } + break; + case Node::TypeAlias: + extra << "alias"; + break; + case Node::Property: { + auto propertyNode = static_cast<const PropertyNode *>(node); + if (propertyNode->propertyType() == PropertyNode::PropertyType::BindableProperty) + extra << "bindable"; + if (!propertyNode->isWritable()) + extra << "read-only"; + } + break; + case Node::QmlProperty: { + auto qmlProperty = static_cast<const QmlPropertyNode *>(node); + if (qmlProperty->isDefault()) + extra << u"default"_s; + // Call non-const overloads to ensure attributes are fetched from + // associated C++ properties + else if (const_cast<QmlPropertyNode *>(qmlProperty)->isReadOnly()) + extra << u"read-only"_s; + else if (const_cast<QmlPropertyNode *>(qmlProperty)->isRequired()) + extra << u"required"_s; + else if (!qmlProperty->defaultValue().isEmpty()) { + extra << u"default: "_s + qmlProperty->defaultValue(); + } + break; + } + default: + break; + } + } + + // Add status for both Summary and Details + if (auto status = nodeStatusAsString(node)) { + if (!extra.isEmpty()) + extra.last() += ','_L1; + extra << *status; + } + + QString extraStr = extra.join(QLatin1Char(' ')); + if (!extraStr.isEmpty()) { + extraStr.prepend(style == Section::Details ? '[' : '('); + extraStr.append(style == Section::Details ? ']' : ')'); + } + + return extraStr; +} + +static const QString samp = QLatin1String("&"); +static const QString slt = QLatin1String("<"); +static const QString sgt = QLatin1String(">"); +static const QString squot = QLatin1String("""); + +QString CodeMarker::protect(const QString &str) +{ + qsizetype n = str.size(); + QString marked; + marked.reserve(n * 2 + 30); + const QChar *data = str.constData(); + for (int i = 0; i != n; ++i) { + switch (data[i].unicode()) { + case '&': + marked += samp; + break; + case '<': + marked += slt; + break; + case '>': + marked += sgt; + break; + case '"': + marked += squot; + break; + default: + marked += data[i]; + } + } + return marked; +} + +void CodeMarker::appendProtectedString(QString *output, QStringView str) +{ + qsizetype n = str.size(); + output->reserve(output->size() + n * 2 + 30); + const QChar *data = str.constData(); + for (int i = 0; i != n; ++i) { + switch (data[i].unicode()) { + case '&': + *output += samp; + break; + case '<': + *output += slt; + break; + case '>': + *output += sgt; + break; + case '"': + *output += squot; + break; + default: + *output += data[i]; + } + } +} + +QString CodeMarker::typified(const QString &string, bool trailingSpace) +{ + QString result; + QString pendingWord; + + for (int i = 0; i <= string.size(); ++i) { + QChar ch; + if (i != string.size()) + ch = string.at(i); + + QChar lower = ch.toLower(); + if ((lower >= QLatin1Char('a') && lower <= QLatin1Char('z')) || ch.digitValue() >= 0 + || ch == QLatin1Char('_') || ch == QLatin1Char(':')) { + pendingWord += ch; + } else { + if (!pendingWord.isEmpty()) { + bool isProbablyType = (pendingWord != QLatin1String("const")); + if (isProbablyType) + result += QLatin1String("<@type>"); + result += pendingWord; + if (isProbablyType) + result += QLatin1String("</@type>"); + } + pendingWord.clear(); + + switch (ch.unicode()) { + case '\0': + break; + case '&': + result += QLatin1String("&"); + break; + case '<': + result += QLatin1String("<"); + break; + case '>': + result += QLatin1String(">"); + break; + default: + result += ch; + } + } + } + if (trailingSpace && string.size()) { + if (!string.endsWith(QLatin1Char('*')) && !string.endsWith(QLatin1Char('&'))) + result += QLatin1Char(' '); + } + return result; +} + +QString CodeMarker::taggedNode(const Node *node) +{ + QString tag; + const QString &name = node->name(); + + switch (node->nodeType()) { + case Node::Namespace: + tag = QLatin1String("@namespace"); + break; + case Node::Class: + case Node::Struct: + case Node::Union: + tag = QLatin1String("@class"); + break; + case Node::Enum: + tag = QLatin1String("@enum"); + break; + case Node::TypeAlias: + case Node::Typedef: + tag = QLatin1String("@typedef"); + break; + case Node::Function: + tag = QLatin1String("@function"); + break; + case Node::Property: + tag = QLatin1String("@property"); + break; + case Node::QmlType: + tag = QLatin1String("@property"); + break; + case Node::Page: + tag = QLatin1String("@property"); + break; + default: + tag = QLatin1String("@unknown"); + break; + } + return (QLatin1Char('<') + tag + QLatin1Char('>') + protect(name) + QLatin1String("</") + tag + + QLatin1Char('>')); +} + +QString CodeMarker::taggedQmlNode(const Node *node) +{ + QString tag; + if (node->isFunction()) { + const auto *fn = static_cast<const FunctionNode *>(node); + switch (fn->metaness()) { + case FunctionNode::QmlSignal: + tag = QLatin1String("@signal"); + break; + case FunctionNode::QmlSignalHandler: + tag = QLatin1String("@signalhandler"); + break; + case FunctionNode::QmlMethod: + tag = QLatin1String("@method"); + break; + default: + tag = QLatin1String("@unknown"); + break; + } + } else if (node->isQmlProperty()) { + tag = QLatin1String("@property"); + } else { + tag = QLatin1String("@unknown"); + } + return QLatin1Char('<') + tag + QLatin1Char('>') + protect(node->name()) + QLatin1String("</") + + tag + QLatin1Char('>'); +} + +QString CodeMarker::linkTag(const Node *node, const QString &body) +{ + return QLatin1String("<@link node=\"") + stringForNode(node) + QLatin1String("\">") + body + + QLatin1String("</@link>"); +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/codemarker.h b/src/qdoc/qdoc/src/qdoc/codemarker.h new file mode 100644 index 000000000..af668b650 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/codemarker.h @@ -0,0 +1,67 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef CODEMARKER_H +#define CODEMARKER_H + +#include "atom.h" +#include "sections.h" + +QT_BEGIN_NAMESPACE + +class CodeMarker +{ +public: + CodeMarker(); + virtual ~CodeMarker(); + + virtual void initializeMarker(); + virtual void terminateMarker(); + virtual bool recognizeCode(const QString & /*code*/) { return true; } + virtual bool recognizeExtension(const QString & /*extension*/) { return true; } + virtual bool recognizeLanguage(const QString & /*language*/) { return false; } + [[nodiscard]] virtual Atom::AtomType atomType() const { return Atom::Code; } + virtual QString markedUpCode(const QString &code, const Node * /*relative*/, + const Location & /*location*/) + { + return protect(code); + } + virtual QString markedUpSynopsis(const Node * /*node*/, const Node * /*relative*/, + Section::Style /*style*/) + { + return QString(); + } + virtual QString markedUpQmlItem(const Node *, bool) { return QString(); } + virtual QString markedUpName(const Node * /*node*/) { return QString(); } + virtual QString markedUpEnumValue(const QString & /*enumValue*/, const Node * /*relative*/) + { + return QString(); + } + virtual QString markedUpInclude(const QString & /*include*/) { return QString(); } + + static void initialize(); + static void terminate(); + static CodeMarker *markerForCode(const QString &code); + static CodeMarker *markerForFileName(const QString &fileName); + static CodeMarker *markerForLanguage(const QString &lang); + static const Node *nodeForString(const QString &string); + static QString stringForNode(const Node *node); + static QString extraSynopsis(const Node *node, Section::Style style); + + QString typified(const QString &string, bool trailingSpace = false); + +protected: + static QString protect(const QString &string); + static void appendProtectedString(QString *output, QStringView str); + QString taggedNode(const Node *node); + QString taggedQmlNode(const Node *node); + QString linkTag(const Node *node, const QString &body); + +private: + static QString s_defaultLang; + static QList<CodeMarker *> s_markers; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/codeparser.cpp b/src/qdoc/qdoc/src/qdoc/codeparser.cpp new file mode 100644 index 000000000..ad35e6d65 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/codeparser.cpp @@ -0,0 +1,139 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "codeparser.h" + +#include "config.h" +#include "generator.h" +#include "node.h" +#include "qdocdatabase.h" + +#include <QtCore/qregularexpression.h> + +QT_BEGIN_NAMESPACE + +QList<CodeParser *> CodeParser::s_parsers; + +/*! + The constructor adds this code parser to the static + list of code parsers. + */ +CodeParser::CodeParser() +{ + m_qdb = QDocDatabase::qdocDB(); + s_parsers.prepend(this); +} + +/*! + The destructor removes this code parser from the static + list of code parsers. + */ +CodeParser::~CodeParser() +{ + s_parsers.removeAll(this); +} + +/*! + Terminating a code parser is trivial. + */ +void CodeParser::terminateParser() +{ + // nothing. +} + +/*! + All the code parsers in the static list are initialized here, + after the qdoc configuration variables have been set. + */ +void CodeParser::initialize() +{ + for (const auto &parser : std::as_const(s_parsers)) + parser->initializeParser(); +} + +/*! + All the code parsers in the static list are terminated here. + */ +void CodeParser::terminate() +{ + for (const auto parser : s_parsers) + parser->terminateParser(); +} + +CodeParser *CodeParser::parserForLanguage(const QString &language) +{ + for (const auto parser : std::as_const(s_parsers)) { + if (parser->language() == language) + return parser; + } + return nullptr; +} + +CodeParser *CodeParser::parserForSourceFile(const QString &filePath) +{ + QString fileName = QFileInfo(filePath).fileName(); + + for (const auto &parser : s_parsers) { + const QStringList sourcePatterns = parser->sourceFileNameFilter(); + for (const QString &pattern : sourcePatterns) { + auto re = QRegularExpression::fromWildcard(pattern, Qt::CaseInsensitive); + if (re.match(fileName).hasMatch()) + return parser; + } + } + return nullptr; +} + +/*! + \internal + */ +void CodeParser::extractPageLinkAndDesc(QStringView arg, QString *link, QString *desc) +{ + static const QRegularExpression bracedRegExp( + QRegularExpression::anchoredPattern(QLatin1String(R"(\{([^{}]*)\}(?:\{([^{}]*)\})?)"))); + auto match = bracedRegExp.matchView(arg); + if (match.hasMatch()) { + *link = match.captured(1); + *desc = match.captured(2); + if (desc->isEmpty()) + *desc = *link; + } else { + qsizetype spaceAt = arg.indexOf(QLatin1Char(' ')); + if (arg.contains(QLatin1String(".html")) && spaceAt != -1) { + *link = arg.left(spaceAt).trimmed().toString(); + *desc = arg.mid(spaceAt).trimmed().toString(); + } else { + *link = arg.toString(); + *desc = *link; + } + } +} + +/*! + \internal + */ +void CodeParser::setLink(Node *node, Node::LinkType linkType, const QString &arg) +{ + QString link; + QString desc; + extractPageLinkAndDesc(arg, &link, &desc); + node->setLink(linkType, link, desc); +} + +/*! + \brief Test for whether a doc comment warrants warnings. + + Returns true if qdoc should report that it has found something + wrong with the qdoc comment in \a doc. Sometimes, qdoc should + not report the warning, for example, when the comment contains + the \c internal command, which normally means qdoc will not use + the comment in the documentation anyway, so there is no point + in reporting warnings about it. + */ +bool CodeParser::isWorthWarningAbout(const Doc &doc) +{ + return (Config::instance().showInternal() + || !doc.metaCommandsUsed().contains(QStringLiteral("internal"))); +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/codeparser.h b/src/qdoc/qdoc/src/qdoc/codeparser.h new file mode 100644 index 000000000..51d1ac2a4 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/codeparser.h @@ -0,0 +1,142 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef CODEPARSER_H +#define CODEPARSER_H + +#include "node.h" + +#include <QtCore/qset.h> + +QT_BEGIN_NAMESPACE + +#define COMMAND_ABSTRACT QLatin1String("abstract") +#define COMMAND_CLASS QLatin1String("class") +#define COMMAND_COMPARES QLatin1String("compares") +#define COMMAND_COMPARESWITH QLatin1String("compareswith") +#define COMMAND_DEFAULT QLatin1String("default") +#define COMMAND_DEPRECATED QLatin1String("deprecated") // ### don't document +#define COMMAND_DONTDOCUMENT QLatin1String("dontdocument") +#define COMMAND_ENUM QLatin1String("enum") +#define COMMAND_EXAMPLE QLatin1String("example") +#define COMMAND_EXTERNALPAGE QLatin1String("externalpage") +#define COMMAND_FN QLatin1String("fn") +#define COMMAND_GROUP QLatin1String("group") +#define COMMAND_HEADERFILE QLatin1String("headerfile") +#define COMMAND_INGROUP QLatin1String("ingroup") +#define COMMAND_INHEADERFILE QLatin1String("inheaderfile") +#define COMMAND_INMODULE QLatin1String("inmodule") // ### don't document +#define COMMAND_INPUBLICGROUP QLatin1String("inpublicgroup") +#define COMMAND_INQMLMODULE QLatin1String("inqmlmodule") +#define COMMAND_INTERNAL QLatin1String("internal") +#define COMMAND_MACRO QLatin1String("macro") +#define COMMAND_MODULE QLatin1String("module") +#define COMMAND_MODULESTATE QLatin1String("modulestate") +#define COMMAND_NAMESPACE QLatin1String("namespace") +#define COMMAND_NEXTPAGE QLatin1String("nextpage") +#define COMMAND_NOAUTOLIST QLatin1String("noautolist") +#define COMMAND_NONREENTRANT QLatin1String("nonreentrant") +#define COMMAND_OBSOLETE QLatin1String("obsolete") +#define COMMAND_OVERLOAD QLatin1String("overload") +#define COMMAND_PAGE QLatin1String("page") +#define COMMAND_PRELIMINARY QLatin1String("preliminary") +#define COMMAND_PREVIOUSPAGE QLatin1String("previouspage") +#define COMMAND_PROPERTY QLatin1String("property") +#define COMMAND_QMLABSTRACT QLatin1String("qmlabstract") +#define COMMAND_QMLATTACHEDMETHOD QLatin1String("qmlattachedmethod") +#define COMMAND_QMLATTACHEDPROPERTY QLatin1String("qmlattachedproperty") +#define COMMAND_QMLATTACHEDSIGNAL QLatin1String("qmlattachedsignal") +#define COMMAND_QMLVALUETYPE QLatin1String("qmlvaluetype") +#define COMMAND_QMLCLASS QLatin1String("qmlclass") +#define COMMAND_QMLDEFAULT QLatin1String("qmldefault") +#define COMMAND_QMLENUMERATORSFROM QLatin1String("qmlenumeratorsfrom") +#define COMMAND_QMLINHERITS QLatin1String("inherits") +#define COMMAND_QMLINSTANTIATES QLatin1String("instantiates") // TODO Qt 7.0.0 - Remove: Deprecated since 6.8. +#define COMMAND_QMLMETHOD QLatin1String("qmlmethod") +#define COMMAND_QMLMODULE QLatin1String("qmlmodule") +#define COMMAND_QMLNATIVETYPE QLatin1String("nativetype") +#define COMMAND_QMLPROPERTY QLatin1String("qmlproperty") +#define COMMAND_QMLPROPERTYGROUP QLatin1String("qmlpropertygroup") +#define COMMAND_QMLREADONLY QLatin1String("readonly") +#define COMMAND_QMLREQUIRED QLatin1String("required") +#define COMMAND_QMLSIGNAL QLatin1String("qmlsignal") +#define COMMAND_QMLTYPE QLatin1String("qmltype") +#define COMMAND_QTCMAKEPACKAGE QLatin1String("qtcmakepackage") +#define COMMAND_QTCMAKETARGETITEM QLatin1String("qtcmaketargetitem") +#define COMMAND_QTVARIABLE QLatin1String("qtvariable") +#define COMMAND_REENTRANT QLatin1String("reentrant") +#define COMMAND_REIMP QLatin1String("reimp") +#define COMMAND_RELATES QLatin1String("relates") +#define COMMAND_SINCE QLatin1String("since") +#define COMMAND_STRUCT QLatin1String("struct") +#define COMMAND_SUBTITLE QLatin1String("subtitle") +#define COMMAND_STARTPAGE QLatin1String("startpage") +#define COMMAND_THREADSAFE QLatin1String("threadsafe") +#define COMMAND_TITLE QLatin1String("title") +#define COMMAND_TYPEALIAS QLatin1String("typealias") +#define COMMAND_TYPEDEF QLatin1String("typedef") +#define COMMAND_VARIABLE QLatin1String("variable") +#define COMMAND_VERSION QLatin1String("version") +#define COMMAND_UNION QLatin1String("union") +#define COMMAND_WRAPPER QLatin1String("wrapper") +#define COMMAND_ATTRIBUTION QLatin1String("attribution") + +// deprecated alias of qmlvaluetype +#define COMMAND_QMLBASICTYPE QLatin1String("qmlbasictype") + +class Location; +class QString; +class QDocDatabase; +class CppCodeParser; + +struct UntiedDocumentation { + Doc documentation; + QStringList context; +}; + +struct TiedDocumentation { + Doc documentation; + Node* node; +}; + +class CodeParser +{ +public: + static inline const QSet<QString> common_meta_commands{ + COMMAND_ABSTRACT, COMMAND_DEFAULT, COMMAND_DEPRECATED, COMMAND_INGROUP, + COMMAND_INMODULE, COMMAND_INPUBLICGROUP, COMMAND_INQMLMODULE, COMMAND_INTERNAL, + COMMAND_MODULESTATE, COMMAND_NOAUTOLIST, COMMAND_NONREENTRANT, COMMAND_OBSOLETE, + COMMAND_PRELIMINARY, COMMAND_QMLABSTRACT, COMMAND_QMLDEFAULT, COMMAND_QMLENUMERATORSFROM, COMMAND_QMLINHERITS, + COMMAND_QMLREADONLY, COMMAND_QMLREQUIRED, COMMAND_QTCMAKEPACKAGE, COMMAND_QTCMAKETARGETITEM, + COMMAND_QTVARIABLE, COMMAND_REENTRANT, COMMAND_SINCE, COMMAND_STARTPAGE, COMMAND_SUBTITLE, + COMMAND_THREADSAFE, COMMAND_TITLE, COMMAND_WRAPPER, COMMAND_ATTRIBUTION, + }; + +public: + CodeParser(); + virtual ~CodeParser(); + + virtual void initializeParser() = 0; + virtual void terminateParser(); + virtual QString language() = 0; + virtual QStringList sourceFileNameFilter() = 0; + virtual void parseSourceFile(const Location &location, const QString &filePath, CppCodeParser& cpp_code_parser) = 0; + + static void initialize(); + static void terminate(); + static CodeParser *parserForLanguage(const QString &language); + static CodeParser *parserForSourceFile(const QString &filePath); + static void setLink(Node *node, Node::LinkType linkType, const QString &arg); + static bool isWorthWarningAbout(const Doc &doc); + +protected: + static void extractPageLinkAndDesc(QStringView arg, QString *link, QString *desc); + QDocDatabase *m_qdb {}; + +private: + static QList<CodeParser *> s_parsers; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/collectionnode.cpp b/src/qdoc/qdoc/src/qdoc/collectionnode.cpp new file mode 100644 index 000000000..833a9d037 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/collectionnode.cpp @@ -0,0 +1,104 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "collectionnode.h" + +#include <QtCore/qstringlist.h> + +QT_BEGIN_NAMESPACE + +/*! + \class CollectionNode + \brief A class for holding the members of a collection of doc pages. + */ + +/*! + Appends \a node to the collection node's member list, if + and only if it isn't already in the member list. + */ +void CollectionNode::addMember(Node *node) +{ + if (!m_members.contains(node)) + m_members.append(node); +} + +/*! + Returns \c true if this collection node contains at least + one namespace node. + */ +bool CollectionNode::hasNamespaces() const +{ + return std::any_of(m_members.cbegin(), m_members.cend(), [](const Node *member) { + return member->isClassNode() && member->isInAPI(); + }); +} + +/*! + Returns \c true if this collection node contains at least + one class node. + */ +bool CollectionNode::hasClasses() const +{ + return std::any_of(m_members.cbegin(), m_members.cend(), [](const Node *member) { + return member->isClassNode() && member->isInAPI(); + }); +} + +/*! + \fn template <typename F> NodeMap CollectionNode::getMembers(const F &&predicate) const + + Returns a map containing this collection node's member nodes for which \c + predicate(node) returns \c true. The \a predicate is a function or a + lambda that takes a const Node pointer as an argument and returns a bool. +*/ + +/*! + \fn NodeMap CollectionNode::getMembers(Node::NodeType type) const + + Returns a map containing this collection node's member nodes with + a specified node \a type. +*/ + +/*! + Returns the logical module version. +*/ +QString CollectionNode::logicalModuleVersion() const +{ + QStringList version; + version << m_logicalModuleVersionMajor << m_logicalModuleVersionMinor; + version.removeAll(QString()); + return version.join("."); +} + +/*! + This function accepts the logical module \a info as a string + list. If the logical module info contains the version number, + it splits the version number on the '.' character to get the + major and minor version numbers. Both major and minor version + numbers should be provided, but the minor version number is + not strictly necessary. + */ +void CollectionNode::setLogicalModuleInfo(const QStringList &info) +{ + m_logicalModuleName = info[0]; + if (info.size() > 1) { + QStringList dotSplit = info[1].split(QLatin1Char('.')); + m_logicalModuleVersionMajor = dotSplit[0]; + if (dotSplit.size() > 1) + m_logicalModuleVersionMinor = dotSplit[1]; + else + m_logicalModuleVersionMinor = "0"; + } +} + +/*! + \fn void CollectionNode::setState(const QString &state) + \fn QString CollectionNode::state() + + Sets or gets a description of this module's state. For example, + \e {"Technical Preview"}. This string is used when generating the + module's documentation page and reference pages of the module's + members. +*/ + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/collectionnode.h b/src/qdoc/qdoc/src/qdoc/collectionnode.h new file mode 100644 index 000000000..3756d3534 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/collectionnode.h @@ -0,0 +1,126 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef COLLECTIONNODE_H +#define COLLECTIONNODE_H + +#include "pagenode.h" + +#include <QtCore/qglobal.h> +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +class CollectionNode : public PageNode +{ +public: + CollectionNode(NodeType type, Aggregate *parent, const QString &name) + : PageNode(type, parent, name) + { + } + + [[nodiscard]] bool isCollectionNode() const override { return true; } + [[nodiscard]] QString qtVariable() const override { return m_qtVariable; } + void setQtVariable(const QString &v) override { m_qtVariable = v; } + [[nodiscard]] QString qtCMakeComponent() const override { return m_qtCMakeComponent; } + [[nodiscard]] QString qtCMakeTargetItem() const override { return m_qtCMakeTargetItem; } + void setQtCMakeComponent(const QString &target) override { m_qtCMakeComponent = target; } + void setQtCMakeTargetItem(const QString &target) override { m_qtCMakeTargetItem = target; } + void addMember(Node *node) override; + [[nodiscard]] bool hasNamespaces() const override; + [[nodiscard]] bool hasClasses() const override; + [[nodiscard]] bool wasSeen() const override { return m_seen; } + + [[nodiscard]] QString fullTitle() const override { return title(); } + [[nodiscard]] QString logicalModuleName() const override { return m_logicalModuleName; } + [[nodiscard]] QString logicalModuleVersion() const override; + [[nodiscard]] QString logicalModuleIdentifier() const override + { + return m_logicalModuleName + m_logicalModuleVersionMajor; + } + [[nodiscard]] QString state() const { return m_state; } + + template <typename F> + NodeMap getMembers(F &&predicate) const + { + NodeMap result; + for (const auto &member : std::as_const(m_members)) { + if (std::invoke(predicate, member) && member->isInAPI()) + result.insert(member->name(), member); + } + return result; + } + + NodeMap getMembers(Node::NodeType type) const + { + return getMembers([type](const Node *n) { + return n->nodeType() == type; + }); + } + + void setLogicalModuleInfo(const QStringList &info) override; + void setState(const QString &state) { m_state = state; } + + // REMARK: Those methods are used by QDocDatabase as a performance + // detail to avoid merging a collection node multiple times. They + // should not be addressed in any other part of the code nor + // should their usage appear more than once in QDocDatabase, + // albeit this is not enforced. + // More information are provided in the comment for the definition + // of m_merged. + void markMerged() { m_merged = true; } + bool isMerged() { return m_merged; } + + [[nodiscard]] const NodeList &members() const { return m_members; } + + void markSeen() { m_seen = true; } + void markNotSeen() { m_seen = false; } + +private: + bool m_seen { false }; + // REMARK: This is set by the database when merging the collection + // node and is later used to avoid merging the same collection + // multiple times. + // Currently, collection nodes may come from multiple projects, + // such that to have a complete overview of the members of a + // collection we need to rejoin all members for all instances of + // the "same" collection. + // This is done in QDocDatabase, generally through an external + // method call that is done ad-hoc when a source-of-truth + // collection node is needed. + // As each part of the code that need such a source-of-truth will + // need to merge the node, to avoid the overhead of a relatively + // expensive operation being performed multiple times, we expose + // this detail so that QDocDatabase can avoid performing the + // operation again. + // To avoid the coupling, QDocDatabase could keep track of the + // merged nodes itself, this is a bit less trivial that this + // implementation and doesn't address the source of the problem + // (the multiple merges themselves and the sequencing of the + // related operations) and as such was discarded in favor of this + // simpler implementation. + // Do note that outside the very specific purpose for which this + // member was made, no part of the code should refer to it and its + // associated methods. + // Should this start to be the case, we can switch to the more + // complex encapsulation into QDocDatabase without having to touch + // the outside user of the merges. + // Further down the line, this is expected to go away completely + // as other part of the code are streamlined. + // KLUDGE: Note that this whole exposure is done as a hackish + // solution to QTBUG-104237 and should not be considered final or + // dependable. + bool m_merged { false }; + NodeList m_members {}; + QString m_logicalModuleName {}; + QString m_logicalModuleVersionMajor {}; + QString m_logicalModuleVersionMinor {}; + QString m_qtVariable {}; + QString m_qtCMakeComponent {}; + QString m_qtCMakeTargetItem {}; + QString m_state {}; +}; + +QT_END_NAMESPACE + +#endif // COLLECTIONNODE_H diff --git a/src/qdoc/qdoc/src/qdoc/comparisoncategory.cpp b/src/qdoc/qdoc/src/qdoc/comparisoncategory.cpp new file mode 100644 index 000000000..98f7aaf66 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/comparisoncategory.cpp @@ -0,0 +1,28 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +/*! + \enum ComparisonCategory + \internal + + \value None No comparison is defined. + \value Strong Strong comparison is defined, see std::strong_ordering. + \value Weak Weak comparison is defined, see std::weak_ordering. + \value Partial A partial ordering is defined, see std::partial_ordering. + \value Equality Only (in)equality comparison is defined. +*/ + +/*! + \fn static inline std::string comparisonCategoryAsString(const ComparisonCategory &category) + \internal + + Returns a string representation of the comparison category \a category. +*/ + +/*! + \fn static ComparisonCategory comparisonCategoryFromString(const std::string &string) + \internal + + Returns a matching comparison category for a \a string representation, or + ComparisonCategory::None for an unknown category string. +*/ diff --git a/src/qdoc/qdoc/src/qdoc/comparisoncategory.h b/src/qdoc/qdoc/src/qdoc/comparisoncategory.h new file mode 100644 index 000000000..d3f36654b --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/comparisoncategory.h @@ -0,0 +1,54 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef COMPARISONCATEGORY_H +#define COMPARISONCATEGORY_H + +#include <string> + +QT_BEGIN_NAMESPACE + +enum struct ComparisonCategory : unsigned char { + None, + Strong, + Weak, + Partial, + Equality +}; + +static inline std::string comparisonCategoryAsString(ComparisonCategory category) +{ + switch (category) { + case ComparisonCategory::Strong: + return "strong"; + case ComparisonCategory::Weak: + return "weak"; + case ComparisonCategory::Partial: + return "partial"; + case ComparisonCategory::Equality: + return "equality"; + case ComparisonCategory::None: + [[fallthrough]]; + default: + break; + } + return {}; +} + +static inline ComparisonCategory comparisonCategoryFromString(const std::string &string) +{ + if (string == "strong") + return ComparisonCategory::Strong; + if (string == "weak") + return ComparisonCategory::Weak; + if (string == "partial") + return ComparisonCategory::Partial; + if (string == "equality") + return ComparisonCategory::Equality; + + return ComparisonCategory::None; +} + +QT_END_NAMESPACE + +#endif // COMPARISONCATEGORY_H diff --git a/src/qdoc/qdoc/src/qdoc/config.cpp b/src/qdoc/qdoc/src/qdoc/config.cpp new file mode 100644 index 000000000..de987dae8 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/config.cpp @@ -0,0 +1,1439 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "config.h" +#include "utilities.h" + +#include <QtCore/qdir.h> +#include <QtCore/qfile.h> +#include <QtCore/qtemporaryfile.h> +#include <QtCore/qtextstream.h> +#include <QtCore/qvariant.h> +#include <QtCore/qregularexpression.h> + +QT_BEGIN_NAMESPACE + +QString ConfigStrings::AUTOLINKERRORS = QStringLiteral("autolinkerrors"); +QString ConfigStrings::BUILDVERSION = QStringLiteral("buildversion"); +QString ConfigStrings::CODEINDENT = QStringLiteral("codeindent"); +QString ConfigStrings::CODEPREFIX = QStringLiteral("codeprefix"); +QString ConfigStrings::CODESUFFIX = QStringLiteral("codesuffix"); +QString ConfigStrings::CPPCLASSESPAGE = QStringLiteral("cppclassespage"); +QString ConfigStrings::CPPCLASSESTITLE = QStringLiteral("cppclassestitle"); +QString ConfigStrings::DEFINES = QStringLiteral("defines"); +QString ConfigStrings::DEPENDS = QStringLiteral("depends"); +QString ConfigStrings::DESCRIPTION = QStringLiteral("description"); +QString ConfigStrings::DOCBOOKEXTENSIONS = QStringLiteral("usedocbookextensions"); +QString ConfigStrings::ENDHEADER = QStringLiteral("endheader"); +QString ConfigStrings::EXAMPLEDIRS = QStringLiteral("exampledirs"); +QString ConfigStrings::EXAMPLES = QStringLiteral("examples"); +QString ConfigStrings::EXAMPLESINSTALLPATH = QStringLiteral("examplesinstallpath"); +QString ConfigStrings::EXCLUDEDIRS = QStringLiteral("excludedirs"); +QString ConfigStrings::EXCLUDEFILES = QStringLiteral("excludefiles"); +QString ConfigStrings::EXTRAIMAGES = QStringLiteral("extraimages"); +QString ConfigStrings::FALSEHOODS = QStringLiteral("falsehoods"); +QString ConfigStrings::FORMATTING = QStringLiteral("formatting"); +QString ConfigStrings::HEADERDIRS = QStringLiteral("headerdirs"); +QString ConfigStrings::HEADERS = QStringLiteral("headers"); +QString ConfigStrings::HEADERSCRIPTS = QStringLiteral("headerscripts"); +QString ConfigStrings::HEADERSTYLES = QStringLiteral("headerstyles"); +QString ConfigStrings::HOMEPAGE = QStringLiteral("homepage"); +QString ConfigStrings::HOMETITLE = QStringLiteral("hometitle"); +QString ConfigStrings::IGNOREDIRECTIVES = QStringLiteral("ignoredirectives"); +QString ConfigStrings::IGNORESINCE = QStringLiteral("ignoresince"); +QString ConfigStrings::IGNORETOKENS = QStringLiteral("ignoretokens"); +QString ConfigStrings::IGNOREWORDS = QStringLiteral("ignorewords"); +QString ConfigStrings::IMAGEDIRS = QStringLiteral("imagedirs"); +QString ConfigStrings::INCLUDEPATHS = QStringLiteral("includepaths"); +QString ConfigStrings::INCLUSIVE = QStringLiteral("inclusive"); +QString ConfigStrings::INDEXES = QStringLiteral("indexes"); +QString ConfigStrings::LANDINGPAGE = QStringLiteral("landingpage"); +QString ConfigStrings::LANDINGTITLE = QStringLiteral("landingtitle"); +QString ConfigStrings::LANGUAGE = QStringLiteral("language"); +QString ConfigStrings::LOCATIONINFO = QStringLiteral("locationinfo"); +QString ConfigStrings::LOGPROGRESS = QStringLiteral("logprogress"); +QString ConfigStrings::MACRO = QStringLiteral("macro"); +QString ConfigStrings::MANIFESTMETA = QStringLiteral("manifestmeta"); +QString ConfigStrings::MODULEHEADER = QStringLiteral("moduleheader"); +QString ConfigStrings::NATURALLANGUAGE = QStringLiteral("naturallanguage"); +QString ConfigStrings::NAVIGATION = QStringLiteral("navigation"); +QString ConfigStrings::NOLINKERRORS = QStringLiteral("nolinkerrors"); +QString ConfigStrings::OUTPUTDIR = QStringLiteral("outputdir"); +QString ConfigStrings::OUTPUTFORMATS = QStringLiteral("outputformats"); +QString ConfigStrings::OUTPUTPREFIXES = QStringLiteral("outputprefixes"); +QString ConfigStrings::OUTPUTSUFFIXES = QStringLiteral("outputsuffixes"); +QString ConfigStrings::PROJECT = QStringLiteral("project"); +QString ConfigStrings::REDIRECTDOCUMENTATIONTODEVNULL = + QStringLiteral("redirectdocumentationtodevnull"); +QString ConfigStrings::QHP = QStringLiteral("qhp"); +QString ConfigStrings::QUOTINGINFORMATION = QStringLiteral("quotinginformation"); +QString ConfigStrings::SCRIPTS = QStringLiteral("scripts"); +QString ConfigStrings::SHOWINTERNAL = QStringLiteral("showinternal"); +QString ConfigStrings::SINGLEEXEC = QStringLiteral("singleexec"); +QString ConfigStrings::SOURCEDIRS = QStringLiteral("sourcedirs"); +QString ConfigStrings::SOURCEENCODING = QStringLiteral("sourceencoding"); +QString ConfigStrings::SOURCES = QStringLiteral("sources"); +QString ConfigStrings::SPURIOUS = QStringLiteral("spurious"); +QString ConfigStrings::STYLESHEETS = QStringLiteral("stylesheets"); +QString ConfigStrings::SYNTAXHIGHLIGHTING = QStringLiteral("syntaxhighlighting"); +QString ConfigStrings::TABSIZE = QStringLiteral("tabsize"); +QString ConfigStrings::TAGFILE = QStringLiteral("tagfile"); +QString ConfigStrings::TIMESTAMPS = QStringLiteral("timestamps"); +QString ConfigStrings::TOCTITLES = QStringLiteral("toctitles"); +QString ConfigStrings::TRADEMARKSPAGE = QStringLiteral("trademarkspage"); +QString ConfigStrings::URL = QStringLiteral("url"); +QString ConfigStrings::VERSION = QStringLiteral("version"); +QString ConfigStrings::VERSIONSYM = QStringLiteral("versionsym"); +QString ConfigStrings::FILEEXTENSIONS = QStringLiteral("fileextensions"); +QString ConfigStrings::IMAGEEXTENSIONS = QStringLiteral("imageextensions"); +QString ConfigStrings::QMLTYPESPAGE = QStringLiteral("qmltypespage"); +QString ConfigStrings::QMLTYPESTITLE = QStringLiteral("qmltypestitle"); +QString ConfigStrings::WARNINGLIMIT = QStringLiteral("warninglimit"); + +/*! + An entry in a stack, where each entry is a list + of string values. + */ +class MetaStackEntry +{ +public: + void open(); + void close(); + + QStringList accum; + QStringList next; +}; +Q_DECLARE_TYPEINFO(MetaStackEntry, Q_RELOCATABLE_TYPE); + +/*! + Start accumulating values in a list by appending an empty + string to the list. + */ +void MetaStackEntry::open() +{ + next.append(QString()); +} + +/*! + Stop accumulating values and append the list of accumulated + values to the complete list of accumulated values. + + */ +void MetaStackEntry::close() +{ + accum += next; + next.clear(); +} + +/*! + \class MetaStack + + This class maintains a stack of values of config file variables. +*/ +class MetaStack : private QStack<MetaStackEntry> +{ +public: + MetaStack(); + + void process(QChar ch, const Location &location); + QStringList getExpanded(const Location &location); +}; + +/*! + The default constructor pushes a new stack entry and + opens it. + */ +MetaStack::MetaStack() +{ + push(MetaStackEntry()); + top().open(); +} + +/*! + Processes the character \a ch using the \a location. + It really just builds up a name by appending \a ch to + it. + */ +void MetaStack::process(QChar ch, const Location &location) +{ + if (ch == QLatin1Char('{')) { + push(MetaStackEntry()); + top().open(); + } else if (ch == QLatin1Char('}')) { + if (size() == 1) + location.fatal(QStringLiteral("Unexpected '}'")); + + top().close(); + const QStringList suffixes = pop().accum; + const QStringList prefixes = top().next; + + top().next.clear(); + for (const auto &prefix : prefixes) { + for (const auto &suffix : suffixes) + top().next << prefix + suffix; + } + } else if (ch == QLatin1Char(',') && size() > 1) { + top().close(); + top().open(); + } else { + for (QString &topNext : top().next) + topNext += ch; + } +} + +/*! + Returns the accumulated string values. + */ +QStringList MetaStack::getExpanded(const Location &location) +{ + if (size() > 1) + location.fatal(QStringLiteral("Missing '}'")); + + top().close(); + return top().accum; +} + +const QString Config::dot = QLatin1String("."); +bool Config::m_debug = false; +bool Config::m_atomsDump = false; +bool Config::generateExamples = true; +QString Config::overrideOutputDir; +QString Config::installDir; +QSet<QString> Config::overrideOutputFormats; +QMap<QString, QString> Config::m_extractedDirs; +QStack<QString> Config::m_workingDirs; +QMap<QString, QStringList> Config::m_includeFilesMap; + +/*! + \class ConfigVar + \brief contains all the information for a single config variable in a + .qdocconf file. +*/ + +/*! + Returns this configuration variable as a string. + + If the variable is not defined, returns \a defaultString. + + \note By default, \a defaultString is a null string. + This allows determining whether a configuration variable is + undefined (returns a null string) or defined as empty + (returns a non-null, empty string). +*/ +QString ConfigVar::asString(const QString defaultString) const +{ + if (m_name.isEmpty()) + return defaultString; + + QString result(""); // an empty but non-null string + for (const auto &value : std::as_const(m_values)) { + if (!result.isEmpty() && !result.endsWith(QChar('\n'))) + result.append(QChar(' ')); + result.append(value.m_value); + } + return result; +} + +/*! + Returns this config variable as a string list. +*/ +QStringList ConfigVar::asStringList() const +{ + QStringList result; + for (const auto &value : std::as_const(m_values)) + result << value.m_value; + return result; +} + +/*! + Returns this config variable as a string set. +*/ +QSet<QString> ConfigVar::asStringSet() const +{ + const auto &stringList = asStringList(); + return QSet<QString>(stringList.cbegin(), stringList.cend()); +} + +/*! + Returns this config variable as a boolean. +*/ +bool ConfigVar::asBool() const +{ + return QVariant(asString()).toBool(); +} + +/*! + Returns this configuration variable as an integer; iterates + through the string list, interpreting each + string in the list as an integer and adding it to a total sum. + + Returns 0 if this variable is defined as empty, and + -1 if it's is not defined. + */ +int ConfigVar::asInt() const +{ + const QStringList strs = asStringList(); + if (strs.isEmpty()) + return -1; + + int sum = 0; + for (const auto &str : strs) + sum += str.toInt(); + return sum; +} + +/*! + Appends values to this ConfigVar, and adjusts the ExpandVar + parameters so that they continue to refer to the correct values. +*/ +void ConfigVar::append(const ConfigVar &other) +{ + m_expandVars << other.m_expandVars; + QList<ExpandVar>::Iterator it = m_expandVars.end(); + it -= other.m_expandVars.size(); + std::for_each(it, m_expandVars.end(), [this](ExpandVar &v) { + v.m_valueIndex += m_values.size(); + }); + m_values << other.m_values; + m_location = other.m_location; +} + +/*! + \class Config + \brief The Config class contains the configuration variables + for controlling how qdoc produces documentation. + + Its load() function reads, parses, and processes a qdocconf file. + */ + +/*! + \enum Config::PathFlags + + Flags used for retrieving canonicalized paths from Config. + + \value Validate + Issue a warning for paths that do not exist and + remove them from the returned list. + + \value IncludePaths + Assume the variable contains include paths with + prefixes such as \c{-I} that are to be removed + before canonicalizing and then re-inserted. + + \omitvalue None + + \sa getCanonicalPathList() +*/ + +/*! + Initializes the Config with \a programName and sets all + internal state variables to either default values or to ones + defined in command line arguments \a args. + */ +void Config::init(const QString &programName, const QStringList &args) +{ + m_prog = programName; + processCommandLineOptions(args); + reset(); +} + +Config::~Config() +{ + clear(); +} + +/*! + Clears the location and internal maps for config variables. + */ +void Config::clear() +{ + m_location = Location(); + m_configVars.clear(); + m_includeFilesMap.clear(); + m_excludedPaths.reset(); +} + +/*! + Resets the Config instance - used by load() + */ +void Config::reset() +{ + clear(); + + // Default values + setStringList(CONFIG_CODEINDENT, QStringList("0")); + setStringList(CONFIG_FALSEHOODS, QStringList("0")); + setStringList(CONFIG_HEADERS + dot + CONFIG_FILEEXTENSIONS, QStringList("*.ch *.h *.h++ *.hh *.hpp *.hxx")); + setStringList(CONFIG_SOURCES + dot + CONFIG_FILEEXTENSIONS, QStringList("*.c++ *.cc *.cpp *.cxx *.mm *.qml *.qdoc")); + setStringList(CONFIG_LANGUAGE, QStringList("Cpp")); // i.e. C++ + setStringList(CONFIG_OUTPUTFORMATS, QStringList("HTML")); + setStringList(CONFIG_TABSIZE, QStringList("8")); + setStringList(CONFIG_LOCATIONINFO, QStringList("true")); + + // Publish options from the command line as config variables + const auto setListFlag = [this](const QString &key, bool test) { + setStringList(key, QStringList(test ? QStringLiteral("true") : QStringLiteral("false"))); + }; +#define SET(opt, test) setListFlag(opt, m_parser.isSet(m_parser.test)) + SET(CONFIG_SYNTAXHIGHLIGHTING, highlightingOption); + SET(CONFIG_SHOWINTERNAL, showInternalOption); + SET(CONFIG_SINGLEEXEC, singleExecOption); + SET(CONFIG_REDIRECTDOCUMENTATIONTODEVNULL, redirectDocumentationToDevNullOption); + SET(CONFIG_AUTOLINKERRORS, autoLinkErrorsOption); +#undef SET + m_showInternal = m_configVars.value(CONFIG_SHOWINTERNAL).asBool(); + setListFlag(CONFIG_NOLINKERRORS, + m_parser.isSet(m_parser.noLinkErrorsOption) + || qEnvironmentVariableIsSet("QDOC_NOLINKERRORS")); + + // CONFIG_DEFINES and CONFIG_INCLUDEPATHS are set in load() +} + +/*! + Loads and parses the qdoc configuration file \a fileName. + If a previous project was loaded, this function first resets the + Config instance. Then it calls the other load() function, which + does the loading, parsing, and processing of the configuration file. + */ +void Config::load(const QString &fileName) +{ + // Reset if a previous project was loaded + if (m_configVars.contains(CONFIG_PROJECT)) + reset(); + + load(Location(), fileName); + if (m_location.isEmpty()) + m_location = Location(fileName); + else + m_location.setEtc(true); + + expandVariables(); + + // Add defines and includepaths from command line to their + // respective configuration variables. Values set here are + // always added to what's defined in configuration file. + insertStringList(CONFIG_DEFINES, m_defines); + insertStringList(CONFIG_INCLUDEPATHS, m_includePaths); + + // Prefetch values that are used internally + m_exampleFiles = getCanonicalPathList(CONFIG_EXAMPLES); + m_exampleDirs = getCanonicalPathList(CONFIG_EXAMPLEDIRS); +} + +/*! + Expands other config variables referred to in all stored ConfigVars. +*/ +void Config::expandVariables() +{ + for (auto &configVar : m_configVars) { + for (auto it = configVar.m_expandVars.crbegin(); it != configVar.m_expandVars.crend(); ++it) { + Q_ASSERT(it->m_valueIndex < configVar.m_values.size()); + const QString &key = it->m_var; + const auto &refVar = m_configVars.value(key); + if (refVar.m_name.isEmpty()) { + configVar.m_location.fatal( + QStringLiteral("Environment or configuration variable '%1' undefined") + .arg(it->m_var)); + } else if (!refVar.m_expandVars.empty()) { + configVar.m_location.fatal( + QStringLiteral("Nested variable expansion not allowed"), + QStringLiteral("When expanding '%1' at %2:%3") + .arg(refVar.m_name, refVar.m_location.filePath(), + QString::number(refVar.m_location.lineNo()))); + } + QString expanded; + if (it->m_delim.isNull()) + expanded = m_configVars.value(key).asStringList().join(QString()); + else + expanded = m_configVars.value(key).asStringList().join(it->m_delim); + configVar.m_values[it->m_valueIndex].m_value.insert(it->m_index, expanded); + } + configVar.m_expandVars.clear(); + } +} + +/*! + Sets the \a values of a configuration variable \a var from a string list. + */ +void Config::setStringList(const QString &var, const QStringList &values) +{ + m_configVars.insert(var, ConfigVar(var, values, QDir::currentPath())); +} + +/*! + Adds the \a values from a string list to the configuration variable \a var. + Existing value(s) are kept. +*/ +void Config::insertStringList(const QString &var, const QStringList &values) +{ + m_configVars[var].append(ConfigVar(var, values, QDir::currentPath())); +} + +/*! + Process and store variables from the command line. + */ +void Config::processCommandLineOptions(const QStringList &args) +{ + m_parser.process(args); + + m_defines = m_parser.values(m_parser.defineOption); + m_dependModules = m_parser.values(m_parser.dependsOption); + setIndexDirs(); + setIncludePaths(); + + generateExamples = !m_parser.isSet(m_parser.noExamplesOption); + if (m_parser.isSet(m_parser.installDirOption)) + installDir = m_parser.value(m_parser.installDirOption); + if (m_parser.isSet(m_parser.outputDirOption)) + overrideOutputDir = QDir(m_parser.value(m_parser.outputDirOption)).absolutePath(); + + const auto outputFormats = m_parser.values(m_parser.outputFormatOption); + for (const auto &format : outputFormats) + overrideOutputFormats.insert(format); + m_debug = m_parser.isSet(m_parser.debugOption) || qEnvironmentVariableIsSet("QDOC_DEBUG"); + m_atomsDump = m_parser.isSet(m_parser.atomsDumpOption); + m_showInternal = m_parser.isSet(m_parser.showInternalOption) + || qEnvironmentVariableIsSet("QDOC_SHOW_INTERNAL"); + + if (m_parser.isSet(m_parser.prepareOption)) + m_qdocPass = Prepare; + if (m_parser.isSet(m_parser.generateOption)) + m_qdocPass = Generate; + if (m_debug || m_parser.isSet(m_parser.logProgressOption)) + setStringList(CONFIG_LOGPROGRESS, QStringList("true")); + if (m_parser.isSet(m_parser.timestampsOption)) + setStringList(CONFIG_TIMESTAMPS, QStringList("true")); + if (m_parser.isSet(m_parser.useDocBookExtensions)) + setStringList(CONFIG_DOCBOOKEXTENSIONS, QStringList("true")); +} + +void Config::setIncludePaths() +{ + QDir currentDir = QDir::current(); + const auto addIncludePaths = [this, currentDir](const char *flag, const QStringList &paths) { + for (const auto &path : paths) + m_includePaths << currentDir.absoluteFilePath(path).insert(0, flag); + }; + + addIncludePaths("-I", m_parser.values(m_parser.includePathOption)); +#ifdef QDOC_PASS_ISYSTEM + addIncludePaths("-isystem", m_parser.values(m_parser.includePathSystemOption)); +#endif + addIncludePaths("-F", m_parser.values(m_parser.frameworkOption)); +} + +/*! + Stores paths from -indexdir command line option(s). + */ +void Config::setIndexDirs() +{ + m_indexDirs = m_parser.values(m_parser.indexDirOption); + auto it = std::remove_if(m_indexDirs.begin(), m_indexDirs.end(), + [](const QString &s) { return !QFile::exists(s); }); + + std::for_each(it, m_indexDirs.end(), [](const QString &s) { + qCWarning(lcQdoc) << "Cannot find index directory: " << s; + }); + m_indexDirs.erase(it, m_indexDirs.end()); +} + +/*! + Function to return the correct outputdir for the output \a format. + If \a format is not specified, defaults to 'HTML'. + outputdir can be set using the qdocconf or the command-line + variable -outputdir. + */ +QString Config::getOutputDir(const QString &format) const +{ + QString t; + if (overrideOutputDir.isNull()) + t = m_configVars.value(CONFIG_OUTPUTDIR).asString(); + else + t = overrideOutputDir; + if (m_configVars.value(CONFIG_SINGLEEXEC).asBool()) { + QString project = m_configVars.value(CONFIG_PROJECT).asString(); + t += QLatin1Char('/') + project.toLower(); + } + if (m_configVars.value(format + Config::dot + "nosubdirs").asBool()) { + QString singleOutputSubdir = m_configVars.value(format + Config::dot + "outputsubdir").asString(); + if (singleOutputSubdir.isEmpty()) + singleOutputSubdir = "html"; + t += QLatin1Char('/') + singleOutputSubdir; + } + return QDir::cleanPath(t); +} + +/*! + Function to return the correct outputformats. + outputformats can be set using the qdocconf or the command-line + variable -outputformat. + */ +QSet<QString> Config::getOutputFormats() const +{ + if (overrideOutputFormats.isEmpty()) + return m_configVars.value(CONFIG_OUTPUTFORMATS).asStringSet(); + else + return overrideOutputFormats; +} + +// TODO: [late-canonicalization][pod-configuration] +// The canonicalization for paths is done at the time where they are +// required, and done each time they are requested. +// Instead, config should be parsed to an intermediate format that is +// a POD type that already contains canonicalized representations for +// each element. +// Those representations should provide specific guarantees about +// their format and be representable at the API boundaries. +// +// This would ensure that the correct canonicalization is always +// applied, is applied only once and that dependent sub-logics can be +// written in a way that doesn't require branching or futher +// canonicalization. + +/*! + Returns a path list where all paths from the config variable \a var + are canonicalized. If \a flags contains \c Validate, outputs a warning + for invalid paths. The \c IncludePaths flag is used as a hint to strip + away potential prefixes found in include paths before attempting to + canonicalize. + */ +QStringList Config::getCanonicalPathList(const QString &var, PathFlags flags) const +{ + QStringList result; + const auto &configVar = m_configVars.value(var); + + for (const auto &value : configVar.m_values) { + const QString ¤tPath = value.m_path; + QString rawValue = value.m_value.simplified(); + QString prefix; + + if (flags & IncludePaths) { + const QStringList prefixes = QStringList() + << QLatin1String("-I") + << QLatin1String("-F") + << QLatin1String("-isystem"); + const auto end = std::end(prefixes); + const auto it = + std::find_if(std::begin(prefixes), end, + [&rawValue](const QString &p) { + return rawValue.startsWith(p); + }); + if (it != end) { + prefix = *it; + rawValue.remove(0, it->size()); + if (rawValue.isEmpty()) + continue; + } else { + prefix = prefixes[0]; // -I as default + } + } + + QDir dir(rawValue.trimmed()); + const QString path = dir.path(); + + if (dir.isRelative()) + dir.setPath(currentPath + QLatin1Char('/') + path); + if ((flags & Validate) && !QFileInfo::exists(dir.path())) + configVar.m_location.warning(QStringLiteral("Cannot find file or directory: %1").arg(path)); + else { + const QString canonicalPath = dir.canonicalPath(); + if (!canonicalPath.isEmpty()) + result.append(prefix + canonicalPath); + else if (path.contains(QLatin1Char('*')) || path.contains(QLatin1Char('?'))) + result.append(path); + else + qCDebug(lcQdoc) << + qUtf8Printable(QStringLiteral("%1: Ignored nonexistent path \'%2\'") + .arg(configVar.m_location.toString(), rawValue)); + } + } + return result; +} + +/*! + Calls getRegExpList() with the control variable \a var and + iterates through the resulting list of regular expressions, + concatenating them with extra characters to form a single + QRegularExpression, which is then returned. + + \sa getRegExpList() + */ +QRegularExpression Config::getRegExp(const QString &var) const +{ + QString pattern; + const auto subRegExps = getRegExpList(var); + + for (const auto ®Exp : subRegExps) { + if (!regExp.isValid()) + return regExp; + if (!pattern.isEmpty()) + pattern += QLatin1Char('|'); + pattern += QLatin1String("(?:") + regExp.pattern() + QLatin1Char(')'); + } + if (pattern.isEmpty()) + pattern = QLatin1String("$x"); // cannot match + return QRegularExpression(pattern); +} + +/*! + Looks up the configuration variable \a var in the string list + map, converts the string list to a list of regular expressions, + and returns it. + */ +QList<QRegularExpression> Config::getRegExpList(const QString &var) const +{ + const QStringList strs = m_configVars.value(var).asStringList(); + QList<QRegularExpression> regExps; + for (const auto &str : strs) + regExps += QRegularExpression(str); + return regExps; +} + +/*! + This function is slower than it could be. What it does is + find all the keys that begin with \a var + dot and return + the matching keys in a set, stripped of the matching prefix + and dot. + */ +QSet<QString> Config::subVars(const QString &var) const +{ + QSet<QString> result; + QString varDot = var + QLatin1Char('.'); + for (auto it = m_configVars.constBegin(); it != m_configVars.constEnd(); ++it) { + if (it.key().startsWith(varDot)) { + QString subVar = it.key().mid(varDot.size()); + int dot = subVar.indexOf(QLatin1Char('.')); + if (dot != -1) + subVar.truncate(dot); + result.insert(subVar); + } + } + return result; +} + +/*! + Searches for a path to \a fileName in 'sources', 'sourcedirs', and + 'exampledirs' config variables and returns a full path to the first + match found. If the file is not found, returns an empty string. + */ +QString Config::getIncludeFilePath(const QString &fileName) const +{ + QString ext = QFileInfo(fileName).suffix(); + + if (!m_includeFilesMap.contains(ext)) { + QStringList result = getCanonicalPathList(CONFIG_SOURCES); + result.erase(std::remove_if(result.begin(), result.end(), + [&](const QString &s) { return !s.endsWith(ext); }), + result.end()); + const QStringList dirs = + getCanonicalPathList(CONFIG_SOURCEDIRS) + + getCanonicalPathList(CONFIG_EXAMPLEDIRS); + + for (const auto &dir : dirs) + result += getFilesHere(dir, "*." + ext, location()); + result.removeDuplicates(); + m_includeFilesMap.insert(ext, result); + } + const QStringList &paths = (*m_includeFilesMap.find(ext)); + QString match = fileName; + if (!match.startsWith('/')) + match.prepend('/'); + for (const auto &path : paths) { + if (path.endsWith(match)) + return path; + } + return QString(); +} + +/*! + Builds and returns a list of file pathnames for the file + type specified by \a filesVar (e.g. "headers" or "sources"). + The files are found in the directories specified by + \a dirsVar, and they are filtered by \a defaultNameFilter + if a better filter can't be constructed from \a filesVar. + The directories in \a excludedDirs are avoided. The files + in \a excludedFiles are not included in the return list. + */ +QStringList Config::getAllFiles(const QString &filesVar, const QString &dirsVar, + const QSet<QString> &excludedDirs, + const QSet<QString> &excludedFiles) +{ + QStringList result = getCanonicalPathList(filesVar, Validate); + const QStringList dirs = getCanonicalPathList(dirsVar, Validate); + + const QString nameFilter = m_configVars.value(filesVar + dot + CONFIG_FILEEXTENSIONS).asString(); + + for (const auto &dir : dirs) + result += getFilesHere(dir, nameFilter, location(), excludedDirs, excludedFiles); + return result; +} + +QStringList Config::getExampleQdocFiles(const QSet<QString> &excludedDirs, + const QSet<QString> &excludedFiles) +{ + QStringList result; + const QStringList dirs = getCanonicalPathList("exampledirs"); + const QString nameFilter = " *.qdoc"; + + for (const auto &dir : dirs) + result += getFilesHere(dir, nameFilter, location(), excludedDirs, excludedFiles); + return result; +} + +QStringList Config::getExampleImageFiles(const QSet<QString> &excludedDirs, + const QSet<QString> &excludedFiles) +{ + QStringList result; + const QStringList dirs = getCanonicalPathList("exampledirs"); + const QString nameFilter = m_configVars.value(CONFIG_EXAMPLES + dot + CONFIG_IMAGEEXTENSIONS).asString(); + + for (const auto &dir : dirs) + result += getFilesHere(dir, nameFilter, location(), excludedDirs, excludedFiles); + return result; +} + +// TODO: [misplaced-logic][examples][pod-configuration] +// The definition of how an example is structured and how to find its +// components should not be part of Config or, for that matter, +// CppCodeParser, which is the actual caller of this method. +// Move this method to a more appropriate place as soon as a suitable +// place is available for it. + +/*! + Returns the path to the project file for \a examplePath, or an empty string + if no project file was found. + */ +QString Config::getExampleProjectFile(const QString &examplePath) +{ + QFileInfo fileInfo(examplePath); + QStringList validNames; + validNames << QLatin1String("CMakeLists.txt") + << fileInfo.fileName() + QLatin1String(".pro") + << fileInfo.fileName() + QLatin1String(".qmlproject") + << fileInfo.fileName() + QLatin1String(".pyproject") + << QLatin1String("qbuild.pro"); // legacy + + QString projectFile; + + for (const auto &name : std::as_const(validNames)) { + projectFile = Config::findFile(Location(), m_exampleFiles, m_exampleDirs, + examplePath + QLatin1Char('/') + name); + if (!projectFile.isEmpty()) + return projectFile; + } + + return projectFile; +} + +// TODO: [pod-configuration] +// Remove findFile completely from the configuration. +// External usages of findFile were already removed but a last caller +// of this method exists internally to Config in +// `getExampleProjectFile`. +// That method has to be removed at some point and this method should +// go with it. +// Do notice that FileResolver is the replacement for findFile but it +// is designed, for now, with a scope that does only care about the +// usages of findFile that are outside the Config class. +// More specifically, it was designed to replace only the uses of +// findFile that deal with user provided queries or queries related to +// that. +// The logic that is used internally in Config is the same, but has a +// different conceptual meaning. +// When findFile is permanently removed, it must be considered whether +// FileResolver itself should be used for the same logic or not. + +/*! + \a fileName is the path of the file to find. + + \a files and \a dirs are the lists where we must find the + components of \a fileName. + + \a location is used for obtaining the file and line numbers + for report qdoc errors. + */ +QString Config::findFile(const Location &location, const QStringList &files, + const QStringList &dirs, const QString &fileName, + QString *userFriendlyFilePath) +{ + if (fileName.isEmpty() || fileName.startsWith(QLatin1Char('/'))) { + if (userFriendlyFilePath) + *userFriendlyFilePath = fileName; + return fileName; + } + + QFileInfo fileInfo; + QStringList components = fileName.split(QLatin1Char('?')); + QString firstComponent = components.first(); + + for (const auto &file : files) { + if (file == firstComponent || file.endsWith(QLatin1Char('/') + firstComponent)) { + fileInfo.setFile(file); + if (!fileInfo.exists()) + location.fatal(QStringLiteral("File '%1' does not exist").arg(file)); + break; + } + } + + if (fileInfo.fileName().isEmpty()) { + for (const auto &dir : dirs) { + fileInfo.setFile(QDir(dir), firstComponent); + if (fileInfo.exists()) + break; + } + } + + if (userFriendlyFilePath) + userFriendlyFilePath->clear(); + if (!fileInfo.exists()) + return QString(); + + // <<REMARK: This is actually dead code. It is unclear what it tries + // to do and why but its usage is unnecessary in the current + // codebase. + // Indeed, the whole concept of the "userFriendlyFilePath" is + // removed for file searching. + // It will be removed directly with the whole of findFile, but it + // should not be considered anymore until then. + if (userFriendlyFilePath) { + for (auto c = components.constBegin();;) { + bool isArchive = (c != components.constEnd() - 1); + userFriendlyFilePath->append(*c); + + if (isArchive) { + QString extracted = m_extractedDirs[fileInfo.filePath()]; + + ++c; + fileInfo.setFile(QDir(extracted), *c); + } else { + break; + } + + userFriendlyFilePath->append(QLatin1Char('?')); + } + } + // REMARK>> + + return fileInfo.filePath(); +} + +// TODO: [pod-configuration] +// An intermediate representation for the configuration should only +// contain data that will later be destructured into subsystem that +// care about specific subsets of the configuration and can carry that +// information with them, uniquely. +// Remove copyFile, moving it into whatever will have the unique +// resposability of knowing how to build an output directory for a +// QDoc execution. +// Should copy file being used for not only copying file to the build +// output directory, split its responsabilities into smaller elements +// instead of forcing the logic together. + +/*! + Copies the \a sourceFilePath to the file name constructed by + concatenating \a targetDirPath and the file name from the + \a userFriendlySourceFilePath. \a location is for identifying + the file and line number where a qdoc error occurred. The + constructed output file name is returned. + */ +QString Config::copyFile(const Location &location, const QString &sourceFilePath, + const QString &userFriendlySourceFilePath, const QString &targetDirPath) +{ + // TODO: A copying operation should only be performed on files + // that we assume to be available. Ensure that this is true at the + // API boundary and bubble up the error checking and reporting to + // call-site users. Possibly this will be as simple as + // ResolvedFile, but could not be done at the time of the introduction of + // that type as we first need to encapsulate the logic for + // copying files into an appropriate subsystem and have a better + // understanding of call-site usages. + + QFile inFile(sourceFilePath); + if (!inFile.open(QFile::ReadOnly)) { + location.warning(QStringLiteral("Cannot open input file for copy: '%1': %2") + .arg(sourceFilePath, inFile.errorString())); + return QString(); + } + + // TODO: [non-canonical-representation] + // Similar to other part of QDoc, we do a series of non-intuitive + // checks to canonicalize some multi-format parameter into + // something we can use. + // Understand which of those formats are actually in use and + // provide a canonicalized version that can be requested at the + // API boundary to ensure that correct formatting is used. + // If possible, gradually bubble up the canonicalization until a + // single entry-point in the program exists where the + // canonicalization can be processed to avoid complicating + // intermediate steps. + // ADDENDUM 1: At least one usage of this seems to depend on the + // processing done for files coming from + // Generator::copyTemplateFile, which are expressed as absolute + // paths. This seems to be the only usage that is currently + // needed, hence a temporary new implementation is provided that + // only takes this case into account. + // Do notice that we assume that in this case we always want a + // flat structure, that is, we are copying the file as a direct + // child of the target directory. + // Nonetheless, it is possible that this case will not be needed, + // such that it can be removed later on, or that it will be nedeed + // in multiple places such that an higher level interface for it + // should be provided. + // Furthermoe, it might be possible that there is an edge case + // that is now not considered, as it is unknown, that was + // considered before. + // As it is now unclear what kind of paths are used here, what + // format they have, why they are used and why they have some + // specific format, further processing is avoided but a more + // torough overview of what should is needed must be done when + // more information are gathered and this function is extracted + // away from config. + + QString outFileName{userFriendlySourceFilePath}; + QFileInfo outFileNameInfo{userFriendlySourceFilePath}; + if (outFileNameInfo.isAbsolute()) + outFileName = outFileNameInfo.fileName(); + + outFileName = targetDirPath + "/" + outFileName; + QDir targetDir(targetDirPath); + if (!targetDir.exists()) + targetDir.mkpath("."); + + QFile outFile(outFileName); + if (!outFile.open(QFile::WriteOnly)) { + // TODO: [uncrentralized-warning] + location.warning(QStringLiteral("Cannot open output file for copy: '%1': %2") + .arg(outFileName, outFile.errorString())); + return QString(); + } + + // TODO: There shouldn't be any particular advantage to copying + // the file by readying its content and writing it compared to + // asking the underlying system to do the copy for us. + // Consider simplifying this part by avoiding doing the manual + // work ourselves. + + char buffer[1024]; + qsizetype len; + while ((len = inFile.read(buffer, sizeof(buffer))) > 0) + outFile.write(buffer, len); + return outFileName; +} + +/*! + Finds the largest unicode digit in \a value in the range + 1..7 and returns it. + */ +int Config::numParams(const QString &value) +{ + int max = 0; + for (int i = 0; i != value.size(); ++i) { + uint c = value[i].unicode(); + if (c > 0 && c < 8) + max = qMax(max, static_cast<int>(c)); + } + return max; +} + +/*! + Returns \c true if \a ch is a letter, number, '_', '.', + '{', '}', or ','. + */ +bool Config::isMetaKeyChar(QChar ch) +{ + return ch.isLetterOrNumber() || ch == QLatin1Char('_') || ch == QLatin1Char('.') + || ch == QLatin1Char('{') || ch == QLatin1Char('}') || ch == QLatin1Char(','); +} + +/*! + \a fileName is a master qdocconf file. It contains a list of + qdocconf files and nothing else. Read the list and return it. + */ +QStringList Config::loadMaster(const QString &fileName) +{ + Location location; + QFile fin(fileName); + if (!fin.open(QFile::ReadOnly | QFile::Text)) { + if (!Config::installDir.isEmpty()) { + qsizetype prefix = location.filePath().size() - location.fileName().size(); + fin.setFileName(Config::installDir + QLatin1Char('/') + + fileName.right(fileName.size() - prefix)); + } + if (!fin.open(QFile::ReadOnly | QFile::Text)) + location.fatal(QStringLiteral("Cannot open master qdocconf file '%1': %2") + .arg(fileName, fin.errorString())); + } + QTextStream stream(&fin); + QStringList qdocFiles; + QDir configDir(QFileInfo(fileName).canonicalPath()); + QString line = stream.readLine(); + while (!line.isNull()) { + if (!line.isEmpty()) + qdocFiles.append(QFileInfo(configDir, line).filePath()); + line = stream.readLine(); + } + fin.close(); + return qdocFiles; +} + +/*! + Load, parse, and process a qdoc configuration file. This + function is only called by the other load() function, but + this one is recursive, i.e., it calls itself when it sees + an \c{include} statement in the qdoc configuration file. + */ +void Config::load(Location location, const QString &fileName) +{ + QFileInfo fileInfo(fileName); + pushWorkingDir(fileInfo.canonicalPath()); + static const QRegularExpression keySyntax(QRegularExpression::anchoredPattern(QLatin1String("\\w+(?:\\.\\w+)*"))); + +#define SKIP_CHAR() \ + do { \ + location.advance(c); \ + ++i; \ + c = text.at(i); \ + cc = c.unicode(); \ + } while (0) + +#define SKIP_SPACES() \ + while (c.isSpace() && cc != '\n') \ + SKIP_CHAR() + +#define PUT_CHAR() \ + word += c; \ + SKIP_CHAR(); + + if (location.depth() > 16) + location.fatal(QStringLiteral("Too many nested includes")); + + QFile fin(fileInfo.fileName()); + if (!fin.open(QFile::ReadOnly | QFile::Text)) { + if (!Config::installDir.isEmpty()) { + qsizetype prefix = location.filePath().size() - location.fileName().size(); + fin.setFileName(Config::installDir + QLatin1Char('/') + + fileName.right(fileName.size() - prefix)); + } + if (!fin.open(QFile::ReadOnly | QFile::Text)) + location.fatal( + QStringLiteral("Cannot open file '%1': %2").arg(fileName, fin.errorString())); + } + + QTextStream stream(&fin); + QString text = stream.readAll(); + text += QLatin1String("\n\n"); + text += QLatin1Char('\0'); + fin.close(); + + location.push(fileName); + location.start(); + + int i = 0; + QChar c = text.at(0); + uint cc = c.unicode(); + while (i < text.size()) { + if (cc == 0) { + ++i; + } else if (c.isSpace()) { + SKIP_CHAR(); + } else if (cc == '#') { + do { + SKIP_CHAR(); + } while (cc != '\n'); + } else if (isMetaKeyChar(c)) { + Location keyLoc = location; + bool plus = false; + QStringList rhsValues; + QList<ExpandVar> expandVars; + QString word; + bool inQuote = false; + bool needsExpansion = false; + + MetaStack stack; + do { + stack.process(c, location); + SKIP_CHAR(); + } while (isMetaKeyChar(c)); + + const QStringList keys = stack.getExpanded(location); + SKIP_SPACES(); + + if (keys.size() == 1 && keys.first() == QLatin1String("include")) { + QString includeFile; + + if (cc != '(') + location.fatal(QStringLiteral("Bad include syntax")); + SKIP_CHAR(); + SKIP_SPACES(); + + while (!c.isSpace() && cc != '#' && cc != ')') { + + if (cc == '$') { + QString var; + SKIP_CHAR(); + while (c.isLetterOrNumber() || cc == '_') { + var += c; + SKIP_CHAR(); + } + if (!var.isEmpty()) { + const QByteArray val = qgetenv(var.toLatin1().data()); + if (val.isNull()) { + location.fatal(QStringLiteral("Environment variable '%1' undefined") + .arg(var)); + } else { + includeFile += QString::fromLatin1(val); + } + } + } else { + includeFile += c; + SKIP_CHAR(); + } + } + SKIP_SPACES(); + if (cc != ')') + location.fatal(QStringLiteral("Bad include syntax")); + SKIP_CHAR(); + SKIP_SPACES(); + if (cc != '#' && cc != '\n') + location.fatal(QStringLiteral("Trailing garbage")); + + /* + Here is the recursive call. + */ + load(location, QFileInfo(QDir(m_workingDirs.top()), includeFile).filePath()); + } else { + /* + It wasn't an include statement, so it's something else. + We must see either '=' or '+=' next. If not, fatal error. + */ + if (cc == '+') { + plus = true; + SKIP_CHAR(); + } + if (cc != '=') + location.fatal(QStringLiteral("Expected '=' or '+=' after key")); + SKIP_CHAR(); + SKIP_SPACES(); + + for (;;) { + if (cc == '\\') { + qsizetype metaCharPos; + + SKIP_CHAR(); + if (cc == '\n') { + SKIP_CHAR(); + } else if (cc > '0' && cc < '8') { + word += QChar(c.digitValue()); + SKIP_CHAR(); + } else if ((metaCharPos = QString::fromLatin1("abfnrtv").indexOf(c)) + != -1) { + word += QLatin1Char("\a\b\f\n\r\t\v"[metaCharPos]); + SKIP_CHAR(); + } else { + PUT_CHAR(); + } + } else if (c.isSpace() || cc == '#') { + if (inQuote) { + if (cc == '\n') + location.fatal(QStringLiteral("Unterminated string")); + PUT_CHAR(); + } else { + if (!word.isEmpty() || needsExpansion) { + rhsValues << word; + word.clear(); + needsExpansion = false; + } + if (cc == '\n' || cc == '#') + break; + SKIP_SPACES(); + } + } else if (cc == '"') { + if (inQuote) { + if (!word.isEmpty() || needsExpansion) + rhsValues << word; + word.clear(); + needsExpansion = false; + } + inQuote = !inQuote; + SKIP_CHAR(); + } else if (cc == '$') { + QString var; + QChar delim(' '); + bool braces = false; + SKIP_CHAR(); + if (cc == '{') { + SKIP_CHAR(); + braces = true; + } + while (c.isLetterOrNumber() || cc == '_') { + var += c; + SKIP_CHAR(); + } + if (braces) { + if (cc == ',') { + SKIP_CHAR(); + delim = c; + SKIP_CHAR(); + } + if (cc == '}') + SKIP_CHAR(); + else if (delim == '}') + delim = QChar(); // null delimiter + else + location.fatal(QStringLiteral("Missing '}'")); + } + if (!var.isEmpty()) { + const QByteArray val = qgetenv(var.toLatin1().constData()); + if (val.isNull()) { + expandVars << ExpandVar(rhsValues.size(), word.size(), var, delim); + needsExpansion = true; + } else if (braces) { // ${VAR} inserts content from an env. variable for processing + text.insert(i, QString::fromLatin1(val)); + c = text.at(i); + cc = c.unicode(); + } else { // while $VAR simply reads the value and stores it to a config variable. + word += QString::fromLatin1(val); + } + } + } else { + if (!inQuote && cc == '=') + location.fatal(QStringLiteral("Unexpected '='")); + PUT_CHAR(); + } + } + for (const auto &key : keys) { + if (!keySyntax.match(key).hasMatch()) + keyLoc.fatal(QStringLiteral("Invalid key '%1'").arg(key)); + + ConfigVar configVar(key, rhsValues, QDir::currentPath(), keyLoc, expandVars); + if (plus && m_configVars.contains(key)) { + m_configVars[key].append(configVar); + } else { + m_configVars.insert(key, configVar); + } + } + } + } else { + location.fatal(QStringLiteral("Unexpected character '%1' at beginning of line").arg(c)); + } + } + popWorkingDir(); + +#undef SKIP_CHAR +#undef SKIP_SPACES +#undef PUT_CHAR +} + +bool Config::isFileExcluded(const QString &fileName, const QSet<QString> &excludedFiles) +{ + for (const QString &entry : excludedFiles) { + if (entry.contains(QLatin1Char('*')) || entry.contains(QLatin1Char('?'))) { + QRegularExpression re(QRegularExpression::wildcardToRegularExpression(entry)); + if (re.match(fileName).hasMatch()) + return true; + } + } + return excludedFiles.contains(fileName); +} + +QStringList Config::getFilesHere(const QString &uncleanDir, const QString &nameFilter, + const Location &location, const QSet<QString> &excludedDirs, + const QSet<QString> &excludedFiles) +{ + // TODO: Understand why location is used to branch the + // canonicalization and why the two different methods are used. + QString dir = + location.isEmpty() ? QDir::cleanPath(uncleanDir) : QDir(uncleanDir).canonicalPath(); + QStringList result; + if (excludedDirs.contains(dir)) + return result; + + QDir dirInfo(dir); + + dirInfo.setNameFilters(nameFilter.split(QLatin1Char(' '))); + dirInfo.setSorting(QDir::Name); + dirInfo.setFilter(QDir::Files); + QStringList fileNames = dirInfo.entryList(); + for (const auto &file : std::as_const(fileNames)) { + // TODO: Understand if this is needed and, should it be, if it + // is indeed the only case that should be considered. + if (!file.startsWith(QLatin1Char('~'))) { + QString s = dirInfo.filePath(file); + QString c = QDir::cleanPath(s); + if (!isFileExcluded(c, excludedFiles)) + result.append(c); + } + } + + dirInfo.setNameFilters(QStringList(QLatin1String("*"))); + dirInfo.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); + fileNames = dirInfo.entryList(); + for (const auto &file : fileNames) + result += getFilesHere(dirInfo.filePath(file), nameFilter, location, excludedDirs, + excludedFiles); + return result; +} + +/*! + Set \a dir as the working directory and push it onto the + stack of working directories. + */ +void Config::pushWorkingDir(const QString &dir) +{ + m_workingDirs.push(dir); + QDir::setCurrent(dir); +} + +/*! + Pop the top entry from the stack of working directories. + Set the working directory to the next one on the stack, + if one exists. + */ +void Config::popWorkingDir() +{ + Q_ASSERT(!m_workingDirs.isEmpty()); + m_workingDirs.pop(); + if (!m_workingDirs.isEmpty()) + QDir::setCurrent(m_workingDirs.top()); +} + +const Config::ExcludedPaths& Config::getExcludedPaths() { + if (m_excludedPaths) + return *m_excludedPaths; + + const auto &excludedDirList = getCanonicalPathList(CONFIG_EXCLUDEDIRS); + const auto &excludedFilesList = getCanonicalPathList(CONFIG_EXCLUDEFILES); + + QSet<QString> excludedDirs = QSet<QString>(excludedDirList.cbegin(), excludedDirList.cend()); + QSet<QString> excludedFiles = QSet<QString>(excludedFilesList.cbegin(), excludedFilesList.cend()); + + m_excludedPaths.emplace(ExcludedPaths{excludedDirs, excludedFiles}); + + return *m_excludedPaths; +} + +std::set<Config::HeaderFilePath> Config::getHeaderFiles() { + static QStringList accepted_header_file_extensions{ + "ch", "h", "h++", "hh", "hpp", "hxx" + }; + + const auto& [excludedDirs, excludedFiles] = getExcludedPaths(); + + QStringList headerList = + getAllFiles(CONFIG_HEADERS, CONFIG_HEADERDIRS, excludedDirs, excludedFiles); + + std::set<HeaderFilePath> headers{}; + + for (const auto& header : headerList) { + if (header.contains("doc/snippets")) continue; + + if (!accepted_header_file_extensions.contains(QFileInfo{header}.suffix())) + continue; + + headers.insert(HeaderFilePath{QFileInfo{header}.canonicalPath(), QFileInfo{header}.fileName()}); + } + + return headers; +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/config.h b/src/qdoc/qdoc/src/qdoc/config.h new file mode 100644 index 000000000..30dad4746 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/config.h @@ -0,0 +1,409 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef CONFIG_H +#define CONFIG_H + +#include "location.h" +#include "qdoccommandlineparser.h" +#include "singleton.h" + +#include <QtCore/qmap.h> +#include <QtCore/qset.h> +#include <QtCore/qstack.h> +#include <QtCore/qstringlist.h> + +#include <set> +#include <utility> + +QT_BEGIN_NAMESPACE + +class Config; + +/* + Contains information about a location + where a ConfigVar string needs to be expanded + from another config variable. +*/ +struct ExpandVar +{ + int m_valueIndex {}; + int m_index {}; + QString m_var {}; + QChar m_delim {}; + + ExpandVar(int valueIndex, int index, QString var, const QChar &delim) + : m_valueIndex(valueIndex), m_index(index), m_var(std::move(var)), m_delim(delim) + { + } +}; + +class ConfigVar +{ +public: + struct ConfigValue { + QString m_value; + QString m_path; + }; + + [[nodiscard]] QString asString(const QString defaultString = QString()) const; + [[nodiscard]] QStringList asStringList() const; + [[nodiscard]] QSet<QString> asStringSet() const; + [[nodiscard]] bool asBool() const; + [[nodiscard]] int asInt() const; + [[nodiscard]] const Location &location() const { return m_location; } + + ConfigVar() = default; + ConfigVar(QString name, const QStringList &values, const QString &dir, + const Location &loc = Location(), + const QList<ExpandVar> &expandVars = QList<ExpandVar>()) + : m_name(std::move(name)), m_location(loc), m_expandVars(expandVars) + { + for (const auto &v : values) + m_values << ConfigValue {v, dir}; + } + +private: + void append(const ConfigVar &other); + +private: + QString m_name {}; + QList<ConfigValue> m_values {}; + Location m_location {}; + QList<ExpandVar> m_expandVars {}; + + friend class Config; +}; + +/* + In this multimap, the key is a config variable name. + */ +typedef QMap<QString, ConfigVar> ConfigVarMap; + +class Config : public Singleton<Config> +{ +public: + ~Config(); + + enum QDocPass { Neither, Prepare, Generate }; + + enum PathFlags : unsigned char { + None = 0x0, + // TODO: [unenforced-unclear-validation] + // The Validate flag is used, for example, during the retrival + // of paths in getCanonicalPathList. + // It is unclear what kind of validation it performs, if any, + // and when this validation is required. + // Instead, remove this kind of flag and ensure that any + // amount of required validation is performed during the + // parsing step, if possilbe, and only once. + // Furthemore, ensure any such validation removes some + // uncertainty on dependent subsystems, moving constraints to + // preconditions and expressing them at the API boundaries. + Validate = 0x1, + IncludePaths = 0x2 + }; + + void init(const QString &programName, const QStringList &args); + [[nodiscard]] bool getDebug() const { return m_debug; } + [[nodiscard]] bool getAtomsDump() const { return m_atomsDump; } + [[nodiscard]] bool showInternal() const { return m_showInternal; } + + void clear(); + void reset(); + void load(const QString &fileName); + void setStringList(const QString &var, const QStringList &values); + void insertStringList(const QString &var, const QStringList &values); + + void showHelp(int exitCode = 0) { m_parser.showHelp(exitCode); } + [[nodiscard]] QStringList qdocFiles() const { return m_parser.positionalArguments(); } + [[nodiscard]] const QString &programName() const { return m_prog; } + [[nodiscard]] const Location &location() const { return m_location; } + [[nodiscard]] const ConfigVar &get(const QString &var) const + { + // Avoid injecting default-constructed values to map if var doesn't exist + static ConfigVar empty; + auto it = m_configVars.constFind(var); + return (it != m_configVars.constEnd()) ? *it : empty; + } + [[nodiscard]] QString getOutputDir(const QString &format = QString("HTML")) const; + [[nodiscard]] QSet<QString> getOutputFormats() const; + [[nodiscard]] QStringList getCanonicalPathList(const QString &var, + PathFlags flags = None) const; + [[nodiscard]] QRegularExpression getRegExp(const QString &var) const; + [[nodiscard]] QList<QRegularExpression> getRegExpList(const QString &var) const; + [[nodiscard]] QSet<QString> subVars(const QString &var) const; + QStringList getAllFiles(const QString &filesVar, const QString &dirsVar, + const QSet<QString> &excludedDirs = QSet<QString>(), + const QSet<QString> &excludedFiles = QSet<QString>()); + [[nodiscard]] QString getIncludeFilePath(const QString &fileName) const; + QStringList getExampleQdocFiles(const QSet<QString> &excludedDirs, + const QSet<QString> &excludedFiles); + QStringList getExampleImageFiles(const QSet<QString> &excludedDirs, + const QSet<QString> &excludedFiles); + QString getExampleProjectFile(const QString &examplePath); + + static QStringList loadMaster(const QString &fileName); + static bool isFileExcluded(const QString &fileName, const QSet<QString> &excludedFiles); + static QStringList getFilesHere(const QString &dir, const QString &nameFilter, + const Location &location = Location(), + const QSet<QString> &excludedDirs = QSet<QString>(), + const QSet<QString> &excludedFiles = QSet<QString>()); + static QString findFile(const Location &location, const QStringList &files, + const QStringList &dirs, const QString &fileName, + QString *userFriendlyFilePath = nullptr); + static QString copyFile(const Location &location, const QString &sourceFilePath, + const QString &userFriendlySourceFilePath, + const QString &targetDirPath); + static int numParams(const QString &value); + static void pushWorkingDir(const QString &dir); + static void popWorkingDir(); + + static const QString dot; + + static bool generateExamples; + static QString installDir; + static QString overrideOutputDir; + static QSet<QString> overrideOutputFormats; + + [[nodiscard]] inline bool singleExec() const; + [[nodiscard]] inline bool dualExec() const; + QStringList &defines() { return m_defines; } + QStringList &dependModules() { return m_dependModules; } + QStringList &includePaths() { return m_includePaths; } + QStringList &indexDirs() { return m_indexDirs; } + [[nodiscard]] QString currentDir() const { return m_currentDir; } + void setCurrentDir(const QString &path) { m_currentDir = path; } + [[nodiscard]] QString previousCurrentDir() const { return m_previousCurrentDir; } + void setPreviousCurrentDir(const QString &path) { m_previousCurrentDir = path; } + + void setQDocPass(const QDocPass &pass) { m_qdocPass = pass; }; + [[nodiscard]] bool preparing() const { return (m_qdocPass == Prepare); } + [[nodiscard]] bool generating() const { return (m_qdocPass == Generate); } + + struct ExcludedPaths { + QSet<QString> excluded_directories; + QSet<QString> excluded_files; + }; + const ExcludedPaths& getExcludedPaths(); + + struct HeaderFilePath { + QString path; + QString filename; + + friend bool operator<(const HeaderFilePath& lhs, const HeaderFilePath& rhs) { + return std::tie(lhs.path, lhs.filename) < std::tie(rhs.path, rhs.filename); + } + }; + std::set<HeaderFilePath> getHeaderFiles(); + +private: + void processCommandLineOptions(const QStringList &args); + void setIncludePaths(); + void setIndexDirs(); + void expandVariables(); + + QStringList m_dependModules {}; + QStringList m_defines {}; + QStringList m_includePaths {}; + QStringList m_indexDirs {}; + QStringList m_exampleFiles {}; + QStringList m_exampleDirs {}; + QString m_currentDir {}; + QString m_previousCurrentDir {}; + std::optional<ExcludedPaths> m_excludedPaths{}; + + bool m_showInternal { false }; + static bool m_debug; + + // An option that can be set trough a similarly named command-line option. + // When this is set, every time QDoc parses a block-comment, a + // human-readable presentation of the `Atom`s structure for that + // block will shown to the user. + static bool m_atomsDump; + + static bool isMetaKeyChar(QChar ch); + void load(Location location, const QString &fileName); + + QString m_prog {}; + Location m_location {}; + ConfigVarMap m_configVars {}; + + static QMap<QString, QString> m_extractedDirs; + static QStack<QString> m_workingDirs; + static QMap<QString, QStringList> m_includeFilesMap; + QDocCommandLineParser m_parser {}; + + QDocPass m_qdocPass { Neither }; +}; + +struct ConfigStrings +{ + static QString ALIAS; + static QString AUTOLINKERRORS; + static QString BUILDVERSION; + static QString CODEINDENT; + static QString CODEPREFIX; + static QString CODESUFFIX; + static QString CPPCLASSESPAGE; + static QString CPPCLASSESTITLE; + static QString DEFINES; + static QString DEPENDS; + static QString DESCRIPTION; + static QString DOCBOOKEXTENSIONS; + static QString ENDHEADER; + static QString EXAMPLEDIRS; + static QString EXAMPLES; + static QString EXAMPLESINSTALLPATH; + static QString EXCLUDEDIRS; + static QString EXCLUDEFILES; + static QString EXTRAIMAGES; + static QString FALSEHOODS; + static QString FORMATTING; + static QString HEADERDIRS; + static QString HEADERS; + static QString HEADERSCRIPTS; + static QString HEADERSTYLES; + static QString HOMEPAGE; + static QString HOMETITLE; + static QString IGNOREDIRECTIVES; + static QString IGNORETOKENS; + static QString IGNORESINCE; + static QString IGNOREWORDS; + static QString IMAGEDIRS; + static QString IMAGES; + static QString INCLUDEPATHS; + static QString INCLUSIVE; + static QString INDEXES; + static QString LANDINGPAGE; + static QString LANDINGTITLE; + static QString LANGUAGE; + static QString LOCATIONINFO; + static QString LOGPROGRESS; + static QString MACRO; + static QString MANIFESTMETA; + static QString MODULEHEADER; + static QString NATURALLANGUAGE; + static QString NAVIGATION; + static QString NOLINKERRORS; + static QString OUTPUTDIR; + static QString OUTPUTFORMATS; + static QString OUTPUTPREFIXES; + static QString OUTPUTSUFFIXES; + static QString PROJECT; + static QString REDIRECTDOCUMENTATIONTODEVNULL; + static QString QHP; + static QString QUOTINGINFORMATION; + static QString SCRIPTS; + static QString SHOWINTERNAL; + static QString SINGLEEXEC; + static QString SOURCEDIRS; + static QString SOURCEENCODING; + static QString SOURCES; + static QString SPURIOUS; + static QString STYLESHEETS; + static QString SYNTAXHIGHLIGHTING; + static QString TABSIZE; + static QString TAGFILE; + static QString TIMESTAMPS; + static QString TOCTITLES; + static QString TRADEMARKSPAGE; + static QString URL; + static QString VERSION; + static QString VERSIONSYM; + static QString FILEEXTENSIONS; + static QString IMAGEEXTENSIONS; + static QString QMLTYPESPAGE; + static QString QMLTYPESTITLE; + static QString WARNINGLIMIT; +}; + +#define CONFIG_AUTOLINKERRORS ConfigStrings::AUTOLINKERRORS +#define CONFIG_BUILDVERSION ConfigStrings::BUILDVERSION +#define CONFIG_CODEINDENT ConfigStrings::CODEINDENT +#define CONFIG_CODEPREFIX ConfigStrings::CODEPREFIX +#define CONFIG_CODESUFFIX ConfigStrings::CODESUFFIX +#define CONFIG_CPPCLASSESPAGE ConfigStrings::CPPCLASSESPAGE +#define CONFIG_CPPCLASSESTITLE ConfigStrings::CPPCLASSESTITLE +#define CONFIG_DEFINES ConfigStrings::DEFINES +#define CONFIG_DEPENDS ConfigStrings::DEPENDS +#define CONFIG_DESCRIPTION ConfigStrings::DESCRIPTION +#define CONFIG_DOCBOOKEXTENSIONS ConfigStrings::DOCBOOKEXTENSIONS +#define CONFIG_ENDHEADER ConfigStrings::ENDHEADER +#define CONFIG_EXAMPLEDIRS ConfigStrings::EXAMPLEDIRS +#define CONFIG_EXAMPLES ConfigStrings::EXAMPLES +#define CONFIG_EXAMPLESINSTALLPATH ConfigStrings::EXAMPLESINSTALLPATH +#define CONFIG_EXCLUDEDIRS ConfigStrings::EXCLUDEDIRS +#define CONFIG_EXCLUDEFILES ConfigStrings::EXCLUDEFILES +#define CONFIG_EXTRAIMAGES ConfigStrings::EXTRAIMAGES +#define CONFIG_FALSEHOODS ConfigStrings::FALSEHOODS +#define CONFIG_FORMATTING ConfigStrings::FORMATTING +#define CONFIG_HEADERDIRS ConfigStrings::HEADERDIRS +#define CONFIG_HEADERS ConfigStrings::HEADERS +#define CONFIG_HEADERSCRIPTS ConfigStrings::HEADERSCRIPTS +#define CONFIG_HEADERSTYLES ConfigStrings::HEADERSTYLES +#define CONFIG_HOMEPAGE ConfigStrings::HOMEPAGE +#define CONFIG_HOMETITLE ConfigStrings::HOMETITLE +#define CONFIG_IGNOREDIRECTIVES ConfigStrings::IGNOREDIRECTIVES +#define CONFIG_IGNORESINCE ConfigStrings::IGNORESINCE +#define CONFIG_IGNORETOKENS ConfigStrings::IGNORETOKENS +#define CONFIG_IGNOREWORDS ConfigStrings::IGNOREWORDS +#define CONFIG_IMAGEDIRS ConfigStrings::IMAGEDIRS +#define CONFIG_INCLUDEPATHS ConfigStrings::INCLUDEPATHS +#define CONFIG_INCLUSIVE ConfigStrings::INCLUSIVE +#define CONFIG_INDEXES ConfigStrings::INDEXES +#define CONFIG_LANDINGPAGE ConfigStrings::LANDINGPAGE +#define CONFIG_LANDINGTITLE ConfigStrings::LANDINGTITLE +#define CONFIG_LANGUAGE ConfigStrings::LANGUAGE +#define CONFIG_LOCATIONINFO ConfigStrings::LOCATIONINFO +#define CONFIG_LOGPROGRESS ConfigStrings::LOGPROGRESS +#define CONFIG_MACRO ConfigStrings::MACRO +#define CONFIG_MANIFESTMETA ConfigStrings::MANIFESTMETA +#define CONFIG_MODULEHEADER ConfigStrings::MODULEHEADER +#define CONFIG_NATURALLANGUAGE ConfigStrings::NATURALLANGUAGE +#define CONFIG_NAVIGATION ConfigStrings::NAVIGATION +#define CONFIG_NOLINKERRORS ConfigStrings::NOLINKERRORS +#define CONFIG_OUTPUTDIR ConfigStrings::OUTPUTDIR +#define CONFIG_OUTPUTFORMATS ConfigStrings::OUTPUTFORMATS +#define CONFIG_OUTPUTPREFIXES ConfigStrings::OUTPUTPREFIXES +#define CONFIG_OUTPUTSUFFIXES ConfigStrings::OUTPUTSUFFIXES +#define CONFIG_PROJECT ConfigStrings::PROJECT +#define CONFIG_REDIRECTDOCUMENTATIONTODEVNULL ConfigStrings::REDIRECTDOCUMENTATIONTODEVNULL +#define CONFIG_QHP ConfigStrings::QHP +#define CONFIG_QUOTINGINFORMATION ConfigStrings::QUOTINGINFORMATION +#define CONFIG_SCRIPTS ConfigStrings::SCRIPTS +#define CONFIG_SHOWINTERNAL ConfigStrings::SHOWINTERNAL +#define CONFIG_SINGLEEXEC ConfigStrings::SINGLEEXEC +#define CONFIG_SOURCEDIRS ConfigStrings::SOURCEDIRS +#define CONFIG_SOURCEENCODING ConfigStrings::SOURCEENCODING +#define CONFIG_SOURCES ConfigStrings::SOURCES +#define CONFIG_SPURIOUS ConfigStrings::SPURIOUS +#define CONFIG_STYLESHEETS ConfigStrings::STYLESHEETS +#define CONFIG_SYNTAXHIGHLIGHTING ConfigStrings::SYNTAXHIGHLIGHTING +#define CONFIG_TABSIZE ConfigStrings::TABSIZE +#define CONFIG_TAGFILE ConfigStrings::TAGFILE +#define CONFIG_TIMESTAMPS ConfigStrings::TIMESTAMPS +#define CONFIG_TOCTITLES ConfigStrings::TOCTITLES +#define CONFIG_TRADEMARKSPAGE ConfigStrings::TRADEMARKSPAGE +#define CONFIG_URL ConfigStrings::URL +#define CONFIG_VERSION ConfigStrings::VERSION +#define CONFIG_VERSIONSYM ConfigStrings::VERSIONSYM +#define CONFIG_FILEEXTENSIONS ConfigStrings::FILEEXTENSIONS +#define CONFIG_IMAGEEXTENSIONS ConfigStrings::IMAGEEXTENSIONS +#define CONFIG_QMLTYPESPAGE ConfigStrings::QMLTYPESPAGE +#define CONFIG_QMLTYPESTITLE ConfigStrings::QMLTYPESTITLE +#define CONFIG_WARNINGLIMIT ConfigStrings::WARNINGLIMIT + +inline bool Config::singleExec() const +{ + return m_configVars.value(CONFIG_SINGLEEXEC).asBool(); +} + +inline bool Config::dualExec() const +{ + return !m_configVars.value(CONFIG_SINGLEEXEC).asBool(); +} + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/cppcodemarker.cpp b/src/qdoc/qdoc/src/qdoc/cppcodemarker.cpp new file mode 100644 index 000000000..7fb26db0c --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/cppcodemarker.cpp @@ -0,0 +1,594 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "cppcodemarker.h" + +#include "access.h" +#include "enumnode.h" +#include "functionnode.h" +#include "namespacenode.h" +#include "propertynode.h" +#include "qmlpropertynode.h" +#include "text.h" +#include "tree.h" +#include "typedefnode.h" +#include "variablenode.h" + +#include <QtCore/qdebug.h> +#include <QtCore/qregularexpression.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +/*! + Returns \c true. + */ +bool CppCodeMarker::recognizeCode(const QString & /* code */) +{ + return true; +} + +/*! + Returns \c true if \a ext is any of a list of file extensions + for the C++ language. + */ +bool CppCodeMarker::recognizeExtension(const QString &extension) +{ + QByteArray ext = extension.toLatin1(); + return ext == "c" || ext == "c++" || ext == "qdoc" || ext == "qtt" || ext == "qtx" + || ext == "cc" || ext == "cpp" || ext == "cxx" || ext == "ch" || ext == "h" + || ext == "h++" || ext == "hh" || ext == "hpp" || ext == "hxx"; +} + +/*! + Returns \c true if \a lang is either "C" or "Cpp". + */ +bool CppCodeMarker::recognizeLanguage(const QString &lang) +{ + return lang == QLatin1String("C") || lang == QLatin1String("Cpp"); +} + +/*! + Returns the type of atom used to represent C++ code in the documentation. +*/ +Atom::AtomType CppCodeMarker::atomType() const +{ + return Atom::Code; +} + +QString CppCodeMarker::markedUpCode(const QString &code, const Node *relative, + const Location &location) +{ + return addMarkUp(code, relative, location); +} + +QString CppCodeMarker::markedUpSynopsis(const Node *node, const Node * /* relative */, + Section::Style style) +{ + const int MaxEnumValues = 6; + const FunctionNode *func; + const VariableNode *variable; + const EnumNode *enume; + QString synopsis; + QString name; + + name = taggedNode(node); + if (style != Section::Details) + name = linkTag(node, name); + name = "<@name>" + name + "</@name>"; + + if (style == Section::Details) { + if (!node->isRelatedNonmember() && !node->isProxyNode() && !node->parent()->name().isEmpty() + && !node->parent()->isHeader() && !node->isProperty() && !node->isQmlNode()) { + name.prepend(taggedNode(node->parent()) + "::"); + } + } + + switch (node->nodeType()) { + case Node::Namespace: + case Node::Class: + case Node::Struct: + case Node::Union: + synopsis = Node::nodeTypeString(node->nodeType()); + synopsis += QLatin1Char(' ') + name; + break; + case Node::Function: + func = (const FunctionNode *)node; + if (style == Section::Details) { + auto templateDecl = node->templateDecl(); + if (templateDecl) + synopsis = protect((*templateDecl).to_qstring()) + QLatin1Char(' '); + } + if (style != Section::AllMembers && !func->returnType().isEmpty()) + synopsis += typified(func->returnType(), true); + synopsis += name; + if (!func->isMacroWithoutParams()) { + synopsis += QLatin1Char('('); + if (!func->parameters().isEmpty()) { + const Parameters ¶meters = func->parameters(); + for (int i = 0; i < parameters.count(); ++i) { + if (i > 0) + synopsis += ", "; + QString name = parameters.at(i).name(); + QString type = parameters.at(i).type(); + QString value = parameters.at(i).defaultValue(); + bool trailingSpace = style != Section::AllMembers && !name.isEmpty(); + synopsis += typified(type, trailingSpace); + if (style != Section::AllMembers && !name.isEmpty()) + synopsis += "<@param>" + protect(name) + "</@param>"; + if (style != Section::AllMembers && !value.isEmpty()) + synopsis += " = " + protect(value); + } + } + synopsis += QLatin1Char(')'); + } + if (func->isConst()) + synopsis += " const"; + + if (style == Section::Summary || style == Section::Accessors) { + if (!func->isNonvirtual()) + synopsis.prepend("virtual "); + if (func->isFinal()) + synopsis.append(" final"); + if (func->isOverride()) + synopsis.append(" override"); + if (func->isPureVirtual()) + synopsis.append(" = 0"); + if (func->isRef()) + synopsis.append(" &"); + else if (func->isRefRef()) + synopsis.append(" &&"); + } else if (style == Section::AllMembers) { + if (!func->returnType().isEmpty() && func->returnType() != "void") + synopsis += " : " + typified(func->returnType()); + } else { + if (func->isRef()) + synopsis.append(" &"); + else if (func->isRefRef()) + synopsis.append(" &&"); + } + break; + case Node::Enum: + enume = static_cast<const EnumNode *>(node); + synopsis = "enum "; + if (enume->isScoped()) + synopsis += "class "; + synopsis += name; + if (style == Section::Summary) { + synopsis += " { "; + + QStringList documentedItems = enume->doc().enumItemNames(); + if (documentedItems.isEmpty()) { + const auto &enumItems = enume->items(); + for (const auto &item : enumItems) + documentedItems << item.name(); + } + const QStringList omitItems = enume->doc().omitEnumItemNames(); + for (const auto &item : omitItems) + documentedItems.removeAll(item); + + if (documentedItems.size() > MaxEnumValues) { + // Take the last element and keep it safe, then elide the surplus. + const QString last = documentedItems.last(); + documentedItems = documentedItems.mid(0, MaxEnumValues - 1); + documentedItems += "…"; + documentedItems += last; + } + synopsis += documentedItems.join(QLatin1String(", ")); + + if (!documentedItems.isEmpty()) + synopsis += QLatin1Char(' '); + synopsis += QLatin1Char('}'); + } + break; + case Node::TypeAlias: + if (style == Section::Details) { + auto templateDecl = node->templateDecl(); + if (templateDecl) + synopsis += protect((*templateDecl).to_qstring()) + QLatin1Char(' '); + } + synopsis += name; + break; + case Node::Typedef: + if (static_cast<const TypedefNode *>(node)->associatedEnum()) + synopsis = "flags "; + synopsis += name; + break; + case Node::Property: { + auto property = static_cast<const PropertyNode *>(node); + synopsis = name + " : " + typified(property->qualifiedDataType()); + break; + } + case Node::QmlProperty: { + auto property = static_cast<const QmlPropertyNode *>(node); + synopsis = name + " : " + typified(property->dataType()); + break; + } + case Node::Variable: + variable = static_cast<const VariableNode *>(node); + if (style == Section::AllMembers) { + synopsis = name + " : " + typified(variable->dataType()); + } else { + synopsis = typified(variable->leftType(), true) + name + protect(variable->rightType()); + } + break; + default: + synopsis = name; + } + + QString extra = CodeMarker::extraSynopsis(node, style); + if (!extra.isEmpty()) { + extra.prepend(u"<@extra>"_s); + extra.append(u"</@extra> "_s); + } + + return extra + synopsis; +} + +/*! + */ +QString CppCodeMarker::markedUpQmlItem(const Node *node, bool summary) +{ + QString name = taggedQmlNode(node); + QString synopsis; + + if (summary) { + name = linkTag(node, name); + } else if (node->isQmlProperty()) { + const auto *pn = static_cast<const QmlPropertyNode *>(node); + if (pn->isAttached()) + name.prepend(pn->element() + QLatin1Char('.')); + } + name = "<@name>" + name + "</@name>"; + if (node->isQmlProperty()) { + const auto *pn = static_cast<const QmlPropertyNode *>(node); + synopsis = name + " : " + typified(pn->dataType()); + } else if (node->isFunction(Node::QML)) { + const auto *func = static_cast<const FunctionNode *>(node); + if (!func->returnType().isEmpty()) + synopsis = typified(func->returnType(), true) + name; + else + synopsis = name; + synopsis += QLatin1Char('('); + if (!func->parameters().isEmpty()) { + const Parameters ¶meters = func->parameters(); + for (int i = 0; i < parameters.count(); ++i) { + if (i > 0) + synopsis += ", "; + QString name = parameters.at(i).name(); + QString type = parameters.at(i).type(); + QString paramName; + if (!name.isEmpty()) { + synopsis += typified(type, true); + paramName = name; + } else { + paramName = type; + } + synopsis += "<@param>" + protect(paramName) + "</@param>"; + } + } + synopsis += QLatin1Char(')'); + } else { + synopsis = name; + } + + QString extra = CodeMarker::extraSynopsis(node, summary ? Section::Summary : Section::Details); + if (!extra.isEmpty()) { + extra.prepend(u" <@extra>"_s); + extra.append(u"</@extra>"_s); + } + + return synopsis + extra; +} + +QString CppCodeMarker::markedUpName(const Node *node) +{ + QString name = linkTag(node, taggedNode(node)); + if (node->isFunction() && !node->isMacro()) + name += "()"; + return name; +} + +QString CppCodeMarker::markedUpEnumValue(const QString &enumValue, const Node *relative) +{ + const auto *node = relative->parent(); + + if (relative->isQmlProperty()) { + const auto *qpn = static_cast<const QmlPropertyNode*>(relative); + if (qpn->enumNode() && !enumValue.startsWith("%1."_L1.arg(qpn->enumPrefix()))) + return "%1<@op>.</@op>%2"_L1.arg(qpn->enumPrefix(), enumValue); + } + + if (!relative->isEnumType()) { + return enumValue; + } + + QStringList parts; + while (!node->isHeader() && node->parent()) { + parts.prepend(markedUpName(node)); + if (node->parent() == relative || node->parent()->name().isEmpty()) + break; + node = node->parent(); + } + if (static_cast<const EnumNode *>(relative)->isScoped()) + parts.append(relative->name()); + + parts.append(enumValue); + return parts.join(QLatin1String("<@op>::</@op>")); +} + +QString CppCodeMarker::markedUpInclude(const QString &include) +{ + return "<@preprocessor>#include <<@headerfile>" + include + "</@headerfile>></@preprocessor>"; +} + +/* + @char + @class + @comment + @function + @keyword + @number + @op + @preprocessor + @string + @type +*/ + +QString CppCodeMarker::addMarkUp(const QString &in, const Node * /* relative */, + const Location & /* location */) +{ + static QSet<QString> types{ + QLatin1String("bool"), QLatin1String("char"), QLatin1String("double"), + QLatin1String("float"), QLatin1String("int"), QLatin1String("long"), + QLatin1String("short"), QLatin1String("signed"), QLatin1String("unsigned"), + QLatin1String("uint"), QLatin1String("ulong"), QLatin1String("ushort"), + QLatin1String("uchar"), QLatin1String("void"), QLatin1String("qlonglong"), + QLatin1String("qulonglong"), QLatin1String("qint"), QLatin1String("qint8"), + QLatin1String("qint16"), QLatin1String("qint32"), QLatin1String("qint64"), + QLatin1String("quint"), QLatin1String("quint8"), QLatin1String("quint16"), + QLatin1String("quint32"), QLatin1String("quint64"), QLatin1String("qreal"), + QLatin1String("cond") + }; + + static QSet<QString> keywords{ + QLatin1String("and"), QLatin1String("and_eq"), QLatin1String("asm"), QLatin1String("auto"), + QLatin1String("bitand"), QLatin1String("bitor"), QLatin1String("break"), + QLatin1String("case"), QLatin1String("catch"), QLatin1String("class"), + QLatin1String("compl"), QLatin1String("const"), QLatin1String("const_cast"), + QLatin1String("continue"), QLatin1String("default"), QLatin1String("delete"), + QLatin1String("do"), QLatin1String("dynamic_cast"), QLatin1String("else"), + QLatin1String("enum"), QLatin1String("explicit"), QLatin1String("export"), + QLatin1String("extern"), QLatin1String("false"), QLatin1String("for"), + QLatin1String("friend"), QLatin1String("goto"), QLatin1String("if"), + QLatin1String("include"), QLatin1String("inline"), QLatin1String("monitor"), + QLatin1String("mutable"), QLatin1String("namespace"), QLatin1String("new"), + QLatin1String("not"), QLatin1String("not_eq"), QLatin1String("operator"), + QLatin1String("or"), QLatin1String("or_eq"), QLatin1String("private"), + QLatin1String("protected"), QLatin1String("public"), QLatin1String("register"), + QLatin1String("reinterpret_cast"), QLatin1String("return"), QLatin1String("sizeof"), + QLatin1String("static"), QLatin1String("static_cast"), QLatin1String("struct"), + QLatin1String("switch"), QLatin1String("template"), QLatin1String("this"), + QLatin1String("throw"), QLatin1String("true"), QLatin1String("try"), + QLatin1String("typedef"), QLatin1String("typeid"), QLatin1String("typename"), + QLatin1String("union"), QLatin1String("using"), QLatin1String("virtual"), + QLatin1String("volatile"), QLatin1String("wchar_t"), QLatin1String("while"), + QLatin1String("xor"), QLatin1String("xor_eq"), QLatin1String("synchronized"), + // Qt specific + QLatin1String("signals"), QLatin1String("slots"), QLatin1String("emit") + }; + + QString code = in; + QString out; + QStringView text; + int braceDepth = 0; + int parenDepth = 0; + int i = 0; + int start = 0; + int finish = 0; + QChar ch; + static const QRegularExpression classRegExp(QRegularExpression::anchoredPattern("Qt?(?:[A-Z3]+[a-z][A-Za-z]*|t)")); + static const QRegularExpression functionRegExp(QRegularExpression::anchoredPattern("q([A-Z][a-z]+)+")); + static const QRegularExpression findFunctionRegExp(QStringLiteral("^\\s*\\(")); + bool atEOF = false; + + auto readChar = [&]() { + if (i < code.size()) + ch = code[i++]; + else + atEOF = true; + }; + + readChar(); + while (!atEOF) { + QString tag; + bool target = false; + + if (ch.isLetter() || ch == '_') { + QString ident; + do { + ident += ch; + finish = i; + readChar(); + } while (!atEOF && (ch.isLetterOrNumber() || ch == '_')); + + if (classRegExp.match(ident).hasMatch()) { + tag = QStringLiteral("type"); + } else if (functionRegExp.match(ident).hasMatch()) { + tag = QStringLiteral("func"); + target = true; + } else if (types.contains(ident)) { + tag = QStringLiteral("type"); + } else if (keywords.contains(ident)) { + tag = QStringLiteral("keyword"); + } else if (braceDepth == 0 && parenDepth == 0) { + if (code.indexOf(findFunctionRegExp, i - 1) == i - 1) + tag = QStringLiteral("func"); + target = true; + } + } else if (ch.isDigit()) { + do { + finish = i; + readChar(); + } while (!atEOF && (ch.isLetterOrNumber() || ch == '.' || ch == '\'')); + tag = QStringLiteral("number"); + } else { + switch (ch.unicode()) { + case '+': + case '-': + case '!': + case '%': + case '^': + case '&': + case '*': + case ',': + case '.': + case '<': + case '=': + case '>': + case '?': + case '[': + case ']': + case '|': + case '~': + finish = i; + readChar(); + tag = QStringLiteral("op"); + break; + case '"': + finish = i; + readChar(); + + while (!atEOF && ch != '"') { + if (ch == '\\') + readChar(); + readChar(); + } + finish = i; + readChar(); + tag = QStringLiteral("string"); + break; + case '#': + finish = i; + readChar(); + while (!atEOF && ch != '\n') { + if (ch == '\\') + readChar(); + finish = i; + readChar(); + } + tag = QStringLiteral("preprocessor"); + break; + case '\'': + finish = i; + readChar(); + + while (!atEOF && ch != '\'') { + if (ch == '\\') + readChar(); + readChar(); + } + finish = i; + readChar(); + tag = QStringLiteral("char"); + break; + case '(': + finish = i; + readChar(); + ++parenDepth; + break; + case ')': + finish = i; + readChar(); + --parenDepth; + break; + case ':': + finish = i; + readChar(); + if (!atEOF && ch == ':') { + finish = i; + readChar(); + tag = QStringLiteral("op"); + } + break; + case '/': + finish = i; + readChar(); + if (!atEOF && ch == '/') { + do { + finish = i; + readChar(); + } while (!atEOF && ch != '\n'); + tag = QStringLiteral("comment"); + } else if (ch == '*') { + bool metAster = false; + bool metAsterSlash = false; + + finish = i; + readChar(); + + while (!metAsterSlash) { + if (atEOF) + break; + if (ch == '*') + metAster = true; + else if (metAster && ch == '/') + metAsterSlash = true; + else + metAster = false; + finish = i; + readChar(); + } + tag = QStringLiteral("comment"); + } else { + tag = QStringLiteral("op"); + } + break; + case '{': + finish = i; + readChar(); + braceDepth++; + break; + case '}': + finish = i; + readChar(); + braceDepth--; + break; + default: + finish = i; + readChar(); + } + } + + text = QStringView{code}.mid(start, finish - start); + start = finish; + + if (!tag.isEmpty()) { + out += QStringLiteral("<@"); + out += tag; + if (target) { + out += QStringLiteral(" target=\""); + out += text; + out += QStringLiteral("()\""); + } + out += QStringLiteral(">"); + } + + appendProtectedString(&out, text); + + if (!tag.isEmpty()) { + out += QStringLiteral("</@"); + out += tag; + out += QStringLiteral(">"); + } + } + + if (start < code.size()) { + appendProtectedString(&out, QStringView{code}.mid(start)); + } + + return out; +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/cppcodemarker.h b/src/qdoc/qdoc/src/qdoc/cppcodemarker.h new file mode 100644 index 000000000..b0c5f3615 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/cppcodemarker.h @@ -0,0 +1,35 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef CPPCODEMARKER_H +#define CPPCODEMARKER_H + +#include "codemarker.h" + +QT_BEGIN_NAMESPACE + +class CppCodeMarker : public CodeMarker +{ +public: + CppCodeMarker() = default; + ~CppCodeMarker() override = default; + + bool recognizeCode(const QString &code) override; + bool recognizeExtension(const QString &ext) override; + bool recognizeLanguage(const QString &lang) override; + [[nodiscard]] Atom::AtomType atomType() const override; + QString markedUpCode(const QString &code, const Node *relative, + const Location &location) override; + QString markedUpSynopsis(const Node *node, const Node *relative, Section::Style style) override; + QString markedUpQmlItem(const Node *node, bool summary) override; + QString markedUpName(const Node *node) override; + QString markedUpEnumValue(const QString &enumValue, const Node *relative) override; + QString markedUpInclude(const QString &include) override; + +private: + QString addMarkUp(const QString &protectedCode, const Node *relative, const Location &location); +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/cppcodeparser.cpp b/src/qdoc/qdoc/src/qdoc/cppcodeparser.cpp new file mode 100644 index 000000000..d2e8d7c63 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/cppcodeparser.cpp @@ -0,0 +1,1021 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "cppcodeparser.h" + +#include "access.h" +#include "classnode.h" +#include "clangcodeparser.h" +#include "collectionnode.h" +#include "comparisoncategory.h" +#include "config.h" +#include "examplenode.h" +#include "externalpagenode.h" +#include "functionnode.h" +#include "generator.h" +#include "headernode.h" +#include "namespacenode.h" +#include "qdocdatabase.h" +#include "qmltypenode.h" +#include "qmlpropertynode.h" +#include "sharedcommentnode.h" + +#include <QtCore/qdebug.h> +#include <QtCore/qmap.h> + +#include <algorithm> + +using namespace Qt::Literals::StringLiterals; + +QT_BEGIN_NAMESPACE + +/* + All these can appear in a C++ namespace. Don't add + anything that can't be in a C++ namespace. + */ +static const QMap<QString, Node::NodeType> s_nodeTypeMap{ + { COMMAND_NAMESPACE, Node::Namespace }, { COMMAND_NAMESPACE, Node::Namespace }, + { COMMAND_CLASS, Node::Class }, { COMMAND_STRUCT, Node::Struct }, + { COMMAND_UNION, Node::Union }, { COMMAND_ENUM, Node::Enum }, + { COMMAND_TYPEALIAS, Node::TypeAlias }, { COMMAND_TYPEDEF, Node::Typedef }, + { COMMAND_PROPERTY, Node::Property }, { COMMAND_VARIABLE, Node::Variable } +}; + +typedef bool (Node::*NodeTypeTestFunc)() const; +static const QMap<QString, NodeTypeTestFunc> s_nodeTypeTestFuncMap{ + { COMMAND_NAMESPACE, &Node::isNamespace }, { COMMAND_CLASS, &Node::isClassNode }, + { COMMAND_STRUCT, &Node::isStruct }, { COMMAND_UNION, &Node::isUnion }, + { COMMAND_ENUM, &Node::isEnumType }, { COMMAND_TYPEALIAS, &Node::isTypeAlias }, + { COMMAND_TYPEDEF, &Node::isTypedef }, { COMMAND_PROPERTY, &Node::isProperty }, + { COMMAND_VARIABLE, &Node::isVariable }, +}; + +CppCodeParser::CppCodeParser(FnCommandParser&& parser) + : fn_parser{parser} +{ + Config &config = Config::instance(); + QStringList exampleFilePatterns{config.get(CONFIG_EXAMPLES + + Config::dot + + CONFIG_FILEEXTENSIONS).asStringList()}; + + if (!exampleFilePatterns.isEmpty()) + m_exampleNameFilter = exampleFilePatterns.join(' '); + else + m_exampleNameFilter = "*.cpp *.h *.js *.xq *.svg *.xml *.ui"; + + QStringList exampleImagePatterns{config.get(CONFIG_EXAMPLES + + Config::dot + + CONFIG_IMAGEEXTENSIONS).asStringList()}; + + if (!exampleImagePatterns.isEmpty()) + m_exampleImageFilter = exampleImagePatterns.join(' '); + else + m_exampleImageFilter = "*.png"; + + m_showLinkErrors = !config.get(CONFIG_NOLINKERRORS).asBool(); +} + +/*! + Process the topic \a command found in the \a doc with argument \a arg. + */ +Node *CppCodeParser::processTopicCommand(const Doc &doc, const QString &command, + const ArgPair &arg) +{ + QDocDatabase* database = QDocDatabase::qdocDB(); + + if (command == COMMAND_FN) { + Q_UNREACHABLE(); + } else if (s_nodeTypeMap.contains(command)) { + /* + We should only get in here if the command refers to + something that can appear in a C++ namespace, + i.e. a class, another namespace, an enum, a typedef, + a property or a variable. I think these are handled + this way to allow the writer to refer to the entity + without including the namespace qualifier. + */ + Node::NodeType type = s_nodeTypeMap[command]; + QStringList words = arg.first.split(QLatin1Char(' ')); + QStringList path; + qsizetype idx = 0; + Node *node = nullptr; + + if (type == Node::Variable && words.size() > 1) + idx = words.size() - 1; + path = words[idx].split("::"); + + node = database->findNodeByNameAndType(path, s_nodeTypeTestFuncMap[command]); + // Allow representing a type alias as a class + if (node == nullptr && command == COMMAND_CLASS) { + node = database->findNodeByNameAndType(path, &Node::isTypeAlias); + if (node) { + auto access = node->access(); + auto loc = node->location(); + auto templateDecl = node->templateDecl(); + node = new ClassNode(Node::Class, node->parent(), node->name()); + node->setAccess(access); + node->setLocation(loc); + node->setTemplateDecl(templateDecl); + } + } + if (node == nullptr) { + if (CodeParser::isWorthWarningAbout(doc)) { + doc.location().warning( + QStringLiteral("Cannot find '%1' specified with '\\%2' in any header file") + .arg(arg.first, command)); + } + } else if (node->isAggregate()) { + if (type == Node::Namespace) { + auto *ns = static_cast<NamespaceNode *>(node); + ns->markSeen(); + ns->setWhereDocumented(ns->tree()->camelCaseModuleName()); + } + } + return node; + } else if (command == COMMAND_EXAMPLE) { + if (Config::generateExamples) { + auto *en = new ExampleNode(database->primaryTreeRoot(), arg.first); + en->setLocation(doc.startLocation()); + setExampleFileLists(en); + return en; + } + } else if (command == COMMAND_EXTERNALPAGE) { + auto *epn = new ExternalPageNode(database->primaryTreeRoot(), arg.first); + epn->setLocation(doc.startLocation()); + return epn; + } else if (command == COMMAND_HEADERFILE) { + auto *hn = new HeaderNode(database->primaryTreeRoot(), arg.first); + hn->setLocation(doc.startLocation()); + return hn; + } else if (command == COMMAND_GROUP) { + CollectionNode *cn = database->addGroup(arg.first); + cn->setLocation(doc.startLocation()); + cn->markSeen(); + return cn; + } else if (command == COMMAND_MODULE) { + CollectionNode *cn = database->addModule(arg.first); + cn->setLocation(doc.startLocation()); + cn->markSeen(); + return cn; + } else if (command == COMMAND_QMLMODULE) { + QStringList blankSplit = arg.first.split(QLatin1Char(' ')); + CollectionNode *cn = database->addQmlModule(blankSplit[0]); + cn->setLogicalModuleInfo(blankSplit); + cn->setLocation(doc.startLocation()); + cn->markSeen(); + return cn; + } else if (command == COMMAND_PAGE) { + auto *pn = new PageNode(database->primaryTreeRoot(), arg.first.split(' ').front()); + pn->setLocation(doc.startLocation()); + return pn; + } else if (command == COMMAND_QMLTYPE || + command == COMMAND_QMLVALUETYPE || + command == COMMAND_QMLBASICTYPE) { + auto nodeType = (command == COMMAND_QMLTYPE) ? Node::QmlType : Node::QmlValueType; + QString qmid; + if (auto args = doc.metaCommandArgs(COMMAND_INQMLMODULE); !args.isEmpty()) + qmid = args.first().first; + auto *qcn = database->findQmlTypeInPrimaryTree(qmid, arg.first); + // A \qmlproperty may have already constructed a placeholder type + // without providing a module identifier; allow such cases + if (!qcn && !qmid.isEmpty()) + qcn = database->findQmlTypeInPrimaryTree(QString(), arg.first); + if (!qcn || qcn->nodeType() != nodeType) + qcn = new QmlTypeNode(database->primaryTreeRoot(), arg.first, nodeType); + if (!qmid.isEmpty()) + database->addToQmlModule(qmid, qcn); + qcn->setLocation(doc.startLocation()); + return qcn; + } else if ((command == COMMAND_QMLSIGNAL) || (command == COMMAND_QMLMETHOD) + || (command == COMMAND_QMLATTACHEDSIGNAL) || (command == COMMAND_QMLATTACHEDMETHOD)) { + Q_UNREACHABLE(); + } + return nullptr; +} + +/*! + A QML property argument has the form... + + <type> <QML-type>::<name> + <type> <QML-module>::<QML-type>::<name> + + This function splits the argument into one of those + two forms. The three part form is the old form, which + was used before the creation of Qt Quick 2 and Qt + Components. A <QML-module> is the QML equivalent of a + C++ namespace. So this function splits \a arg on "::" + and stores the parts in \a type, \a module, \a qmlTypeName, + and \a name, and returns \c true. If any part other than + \a module is not found, a qdoc warning is emitted and + false is returned. + + \note The two QML types \e{Component} and \e{QtObject} + never have a module qualifier. + */ +bool CppCodeParser::splitQmlPropertyArg(const QString &arg, QString &type, QString &module, + QString &qmlTypeName, QString &name, + const Location &location) +{ + QStringList blankSplit = arg.split(QLatin1Char(' ')); + if (blankSplit.size() > 1) { + type = blankSplit[0]; + QStringList colonSplit(blankSplit[1].split("::")); + if (colonSplit.size() == 3) { + module = colonSplit[0]; + qmlTypeName = colonSplit[1]; + name = colonSplit[2]; + return true; + } + if (colonSplit.size() == 2) { + module.clear(); + qmlTypeName = colonSplit[0]; + name = colonSplit[1]; + return true; + } + location.warning( + QStringLiteral("Unrecognizable QML module/component qualifier for %1").arg(arg)); + } else { + location.warning(QStringLiteral("Missing property type for %1").arg(arg)); + } + return false; +} + +std::vector<TiedDocumentation> CppCodeParser::processQmlProperties(const UntiedDocumentation &untied) +{ + const Doc &doc = untied.documentation; + const TopicList &topics = doc.topicsUsed(); + if (topics.isEmpty()) + return {}; + + QString arg; + QString type; + QString group; + QString qmlModule; + QString property; + QString qmlTypeName; + + std::vector<TiedDocumentation> tied{}; + + Topic topic = topics.at(0); + arg = topic.m_args; + if (splitQmlPropertyArg(arg, type, qmlModule, qmlTypeName, property, doc.location())) { + qsizetype i = property.indexOf('.'); + if (i != -1) + group = property.left(i); + } + + QDocDatabase *database = QDocDatabase::qdocDB(); + + NodeList sharedNodes; + QmlTypeNode *qmlType = database->findQmlTypeInPrimaryTree(qmlModule, qmlTypeName); + // Note: Constructing a QmlType node by default, as opposed to QmlValueType. + // This may lead to unexpected behavior if documenting \qmlvaluetype's properties + // before the type itself. + if (qmlType == nullptr) { + qmlType = new QmlTypeNode(database->primaryTreeRoot(), qmlTypeName, Node::QmlType); + qmlType->setLocation(doc.startLocation()); + if (!qmlModule.isEmpty()) + database->addToQmlModule(qmlModule, qmlType); + } + + for (const auto &topicCommand : topics) { + QString cmd = topicCommand.m_topic; + arg = topicCommand.m_args; + if ((cmd == COMMAND_QMLPROPERTY) || (cmd == COMMAND_QMLATTACHEDPROPERTY)) { + bool attached = cmd.contains(QLatin1String("attached")); + if (splitQmlPropertyArg(arg, type, qmlModule, qmlTypeName, property, doc.location())) { + if (qmlType != database->findQmlTypeInPrimaryTree(qmlModule, qmlTypeName)) { + doc.startLocation().warning( + QStringLiteral( + "All properties in a group must belong to the same type: '%1'") + .arg(arg)); + continue; + } + QmlPropertyNode *existingProperty = qmlType->hasQmlProperty(property, attached); + if (existingProperty) { + processMetaCommands(doc, existingProperty); + if (!doc.body().isEmpty()) { + doc.startLocation().warning( + QStringLiteral("QML property documented multiple times: '%1'") + .arg(arg), QStringLiteral("also seen here: %1") + .arg(existingProperty->location().toString())); + } + continue; + } + auto *qpn = new QmlPropertyNode(qmlType, property, type, attached); + qpn->setLocation(doc.startLocation()); + qpn->setGenus(Node::QML); + + tied.emplace_back(TiedDocumentation{doc, qpn}); + + sharedNodes << qpn; + } + } else { + doc.startLocation().warning( + QStringLiteral("Command '\\%1'; not allowed with QML property commands") + .arg(cmd)); + } + } + + // Construct a SharedCommentNode (scn) if multiple topics generated + // valid nodes. Note that it's important to do this *after* constructing + // the topic nodes - which need to be written to index before the related + // scn. + if (sharedNodes.size() > 1) { + auto *scn = new SharedCommentNode(qmlType, sharedNodes.size(), group); + scn->setLocation(doc.startLocation()); + + tied.emplace_back(TiedDocumentation{doc, scn}); + + for (const auto n : sharedNodes) + scn->append(n); + scn->sort(); + } + + return tied; +} + +/*! + Process the metacommand \a command in the context of the + \a node associated with the topic command and the \a doc. + \a arg is the argument to the metacommand. + + \a node is guaranteed to be non-null. + */ +void CppCodeParser::processMetaCommand(const Doc &doc, const QString &command, + const ArgPair &argPair, Node *node) +{ + QDocDatabase* database = QDocDatabase::qdocDB(); + + QString arg = argPair.first; + if (command == COMMAND_INHEADERFILE) { + // TODO: [incorrect-constructs][header-arg] + // The emptiness check for arg is required as, + // currently, DocParser fancies passing (without any warning) + // incorrect constructs doen the chain, such as an + // "\inheaderfile" command with no argument. + // + // As it is the case here, we require further sanity checks to + // preserve some of the semantic for the later phases. + // This generally has a ripple effect on the whole codebase, + // making it more complex and increasesing the surface of bugs. + // + // The following emptiness check should be removed as soon as + // DocParser is enhanced with correct semantics. + if (node->isAggregate() && !arg.isEmpty()) + static_cast<Aggregate *>(node)->setIncludeFile(arg); + else + doc.location().warning(QStringLiteral("Ignored '\\%1'").arg(COMMAND_INHEADERFILE)); + } else if (command == COMMAND_COMPARES) { + processComparesCommand(node, arg, doc.location()); + } else if (command == COMMAND_COMPARESWITH) { + if (!node->isClassNode()) + doc.location().warning( + u"Found \\%1 command outside of \\%2 context."_s + .arg(COMMAND_COMPARESWITH, COMMAND_CLASS)); + } else if (command == COMMAND_OVERLOAD) { + /* + Note that this might set the overload flag of the + primary function. This is ok because the overload + flags and overload numbers will be resolved later + in Aggregate::normalizeOverloads(). + */ + if (node->isFunction()) + static_cast<FunctionNode *>(node)->setOverloadFlag(); + else if (node->isSharedCommentNode()) + static_cast<SharedCommentNode *>(node)->setOverloadFlags(); + else + doc.location().warning(QStringLiteral("Ignored '\\%1'").arg(COMMAND_OVERLOAD)); + } else if (command == COMMAND_REIMP) { + if (node->parent() && !node->parent()->isInternal()) { + if (node->isFunction()) { + auto *fn = static_cast<FunctionNode *>(node); + // The clang visitor class will have set the + // qualified name of the overridden function. + // If the name of the overridden function isn't + // set, issue a warning. + if (fn->overridesThis().isEmpty() && CodeParser::isWorthWarningAbout(doc)) { + doc.location().warning( + QStringLiteral("Cannot find base function for '\\%1' in %2()") + .arg(COMMAND_REIMP, node->name()), + QStringLiteral("The function either doesn't exist in any " + "base class with the same signature or it " + "exists but isn't virtual.")); + } + fn->setReimpFlag(); + } else { + doc.location().warning( + QStringLiteral("Ignored '\\%1' in %2").arg(COMMAND_REIMP, node->name())); + } + } + } else if (command == COMMAND_RELATES) { + // REMARK: Generates warnings only; Node instances are + // adopted from the root namespace to other Aggregates + // in a post-processing step, Aggregate::resolveRelates(), + // after all topic commands are processed. + if (node->isAggregate()) { + doc.location().warning("Invalid '\\%1' not allowed in '\\%2'"_L1 + .arg(COMMAND_RELATES, node->nodeTypeString())); + } else if (!node->isRelatedNonmember() && node->parent()->isClassNode()) { + if (!doc.isInternal()) { + doc.location().warning("Invalid '\\%1' ('%2' must be global)"_L1 + .arg(COMMAND_RELATES, node->name())); + } + } + } else if (command == COMMAND_NEXTPAGE) { + CodeParser::setLink(node, Node::NextLink, arg); + } else if (command == COMMAND_PREVIOUSPAGE) { + CodeParser::setLink(node, Node::PreviousLink, arg); + } else if (command == COMMAND_STARTPAGE) { + CodeParser::setLink(node, Node::StartLink, arg); + } else if (command == COMMAND_QMLINHERITS) { + if (node->name() == arg) + doc.location().warning(QStringLiteral("%1 tries to inherit itself").arg(arg)); + else if (node->isQmlType()) { + auto *qmlType = static_cast<QmlTypeNode *>(node); + qmlType->setQmlBaseName(arg); + } + } else if (command == COMMAND_QMLNATIVETYPE || command == COMMAND_QMLINSTANTIATES) { + if (command == COMMAND_QMLINSTANTIATES) + doc.location().report(u"\\instantiates is deprected and will be removed in a future version. Use \\nativetype instead."_s); + // TODO: COMMAND_QMLINSTANTIATES is deprecated since 6.8. Its remains should be removed no later than Qt 7.0.0. + processQmlNativeTypeCommand(node, arg, doc.location()); + } else if (command == COMMAND_DEFAULT) { + if (!node->isQmlProperty()) { + doc.location().warning(QStringLiteral("Ignored '\\%1', applies only to '\\%2'") + .arg(command, COMMAND_QMLPROPERTY)); + } else if (arg.isEmpty()) { + doc.location().warning(QStringLiteral("Expected an argument for '\\%1' (maybe you meant '\\%2'?)") + .arg(command, COMMAND_QMLDEFAULT)); + } else { + static_cast<QmlPropertyNode *>(node)->setDefaultValue(arg); + } + } else if (command == COMMAND_QMLDEFAULT) { + node->markDefault(); + } else if (command == COMMAND_QMLENUMERATORSFROM) { + if (!node->isQmlProperty()) { + doc.location().warning("Ignored '\\%1', applies only to '\\%2'"_L1 + .arg(command, COMMAND_QMLPROPERTY)); + } else if (!static_cast<QmlPropertyNode*>(node)->setEnumNode(argPair.first, argPair.second)) { + doc.location().warning("Failed to find C++ enumeration '%2' passed to \\%1"_L1 + .arg(command, arg), "Use \\value commands instead"_L1); + } + } else if (command == COMMAND_QMLREADONLY) { + node->markReadOnly(true); + } else if (command == COMMAND_QMLREQUIRED) { + if (!node->isQmlProperty()) + doc.location().warning(QStringLiteral("Ignored '\\%1'").arg(COMMAND_QMLREQUIRED)); + else + static_cast<QmlPropertyNode *>(node)->setRequired(); + } else if ((command == COMMAND_QMLABSTRACT) || (command == COMMAND_ABSTRACT)) { + if (node->isQmlType()) + node->setAbstract(true); + } else if (command == COMMAND_DEPRECATED) { + node->setDeprecated(argPair.second); + } else if (command == COMMAND_INGROUP || command == COMMAND_INPUBLICGROUP) { + // Note: \ingroup and \inpublicgroup are the same (and now recognized as such). + database->addToGroup(arg, node); + } else if (command == COMMAND_INMODULE) { + database->addToModule(arg, node); + } else if (command == COMMAND_INQMLMODULE) { + // Handled when parsing topic commands + } else if (command == COMMAND_OBSOLETE) { + node->setStatus(Node::Deprecated); + } else if (command == COMMAND_NONREENTRANT) { + node->setThreadSafeness(Node::NonReentrant); + } else if (command == COMMAND_PRELIMINARY) { + // \internal wins. + if (!node->isInternal()) + node->setStatus(Node::Preliminary); + } else if (command == COMMAND_INTERNAL) { + if (!Config::instance().showInternal()) + node->markInternal(); + } else if (command == COMMAND_REENTRANT) { + node->setThreadSafeness(Node::Reentrant); + } else if (command == COMMAND_SINCE) { + node->setSince(arg); + } else if (command == COMMAND_WRAPPER) { + node->setWrapper(); + } else if (command == COMMAND_THREADSAFE) { + node->setThreadSafeness(Node::ThreadSafe); + } else if (command == COMMAND_TITLE) { + if (!node->setTitle(arg)) + doc.location().warning(QStringLiteral("Ignored '\\%1'").arg(COMMAND_TITLE)); + else if (node->isExample()) + database->addExampleNode(static_cast<ExampleNode *>(node)); + } else if (command == COMMAND_SUBTITLE) { + if (!node->setSubtitle(arg)) + doc.location().warning(QStringLiteral("Ignored '\\%1'").arg(COMMAND_SUBTITLE)); + } else if (command == COMMAND_QTVARIABLE) { + node->setQtVariable(arg); + if (!node->isModule() && !node->isQmlModule()) + doc.location().warning( + QStringLiteral( + "Command '\\%1' is only meaningful in '\\module' and '\\qmlmodule'.") + .arg(COMMAND_QTVARIABLE)); + } else if (command == COMMAND_QTCMAKEPACKAGE) { + if (node->isModule()) + node->setQtCMakeComponent(arg); + else + doc.location().warning( + QStringLiteral("Command '\\%1' is only meaningful in '\\module'.") + .arg(COMMAND_QTCMAKEPACKAGE)); + } else if (command == COMMAND_QTCMAKETARGETITEM) { + if (node->isModule()) + node->setQtCMakeTargetItem(arg); + else + doc.location().warning( + QStringLiteral("Command '\\%1' is only meaningful in '\\module'.") + .arg(COMMAND_QTCMAKETARGETITEM)); + } else if (command == COMMAND_MODULESTATE ) { + if (!node->isModule() && !node->isQmlModule()) { + doc.location().warning( + QStringLiteral( + "Command '\\%1' is only meaningful in '\\module' and '\\qmlmodule'.") + .arg(COMMAND_MODULESTATE)); + } else { + static_cast<CollectionNode*>(node)->setState(arg); + } + } else if (command == COMMAND_NOAUTOLIST) { + if (!node->isCollectionNode() && !node->isExample()) { + doc.location().warning( + QStringLiteral( + "Command '\\%1' is only meaningful in '\\module', '\\qmlmodule', `\\group` and `\\example`.") + .arg(COMMAND_NOAUTOLIST)); + } else { + static_cast<PageNode*>(node)->setNoAutoList(true); + } + } else if (command == COMMAND_ATTRIBUTION) { + // TODO: This condition is not currently exact enough, as it + // will allow any non-aggregate `PageNode` to use the command, + // For example, an `ExampleNode`. + // + // The command is intended only for internal usage by + // "qattributionscanner" and should only work on `PageNode`s + // that are generated from a "\page" command. + // + // It is already possible to provide a more restricted check, + // albeit in a somewhat dirty way. It is not expected that + // this warning will have any particular use. + // If it so happens that a case where the too-broad scope of + // the warning is a problem or hides a bug, modify the + // condition to be restrictive enough. + // Otherwise, wait until a more torough look at QDoc's + // internal representations an way to enable "Attribution + // Pages" is performed before looking at the issue again. + if (!node->isTextPageNode()) { + doc.location().warning(u"Command '\\%1' is only meaningful in '\\%2'"_s.arg(COMMAND_ATTRIBUTION, COMMAND_PAGE)); + } else { static_cast<PageNode*>(node)->markAttribution(); } + } +} + +/*! + \internal + Processes the argument \a arg that's passed to the \\compares command, + and sets the comparison category of the \a node accordingly. + + If the argument is invalid, issue a warning at the location the command + appears through \a loc. +*/ +void CppCodeParser::processComparesCommand(Node *node, const QString &arg, const Location &loc) +{ + if (!node->isClassNode()) { + loc.warning(u"Found \\%1 command outside of \\%2 context."_s.arg(COMMAND_COMPARES, + COMMAND_CLASS)); + return; + } + + if (auto category = comparisonCategoryFromString(arg.toStdString()); + category != ComparisonCategory::None) { + node->setComparisonCategory(category); + } else { + loc.warning(u"Invalid argument to \\%1 command: `%2`"_s.arg(COMMAND_COMPARES, arg), + u"Valid arguments are `strong`, `weak`, `partial`, or `equality`."_s); + } +} + +/*! + The topic command has been processed, and now \a doc and + \a node are passed to this function to get the metacommands + from \a doc and process them one at a time. \a node is the + node where \a doc resides. + */ +void CppCodeParser::processMetaCommands(const Doc &doc, Node *node) +{ + std::vector<Node*> nodes_to_process{}; + if (node->isSharedCommentNode()) { + auto scn = static_cast<SharedCommentNode*>(node); + + nodes_to_process.reserve(scn->count() + 1); + std::copy(scn->collective().cbegin(), scn->collective().cend(), std::back_inserter(nodes_to_process)); + } + + // REMARK: Ordering is important here. If node is a + // SharedCommentNode it MUST be processed after all its child + // nodes. + // Failure to do so can incur in incorrect warnings. + // For example, if a shared documentation has a "\relates" command. + // When the command is processed for the SharedCommentNode it will + // apply to all its child nodes. + // If a child node is processed after the SharedCommentNode that + // contains it, that "\relates" command will be considered applied + // already, resulting in a warning. + nodes_to_process.push_back(node); + + const QStringList metaCommandsUsed = doc.metaCommandsUsed().values(); + for (const auto &command : metaCommandsUsed) { + const ArgList args = doc.metaCommandArgs(command); + for (const auto &arg : args) { + std::for_each(nodes_to_process.cbegin(), nodes_to_process.cend(), [this, doc, command, arg](auto node){ + processMetaCommand(doc, command, arg, node); + }); + } + } +} + +/*! + Parse QML signal/method topic commands. + */ +FunctionNode *CppCodeParser::parseOtherFuncArg(const QString &topic, const Location &location, + const QString &funcArg) +{ + QString funcName; + QString returnType; + + qsizetype leftParen = funcArg.indexOf(QChar('(')); + if (leftParen > 0) + funcName = funcArg.left(leftParen); + else + funcName = funcArg; + qsizetype firstBlank = funcName.indexOf(QChar(' ')); + if (firstBlank > 0) { + returnType = funcName.left(firstBlank); + funcName = funcName.right(funcName.size() - firstBlank - 1); + } + + QStringList colonSplit(funcName.split("::")); + if (colonSplit.size() < 2) { + QString msg = "Unrecognizable QML module/component qualifier for " + funcArg; + location.warning(msg.toLatin1().data()); + return nullptr; + } + QString moduleName; + QString elementName; + if (colonSplit.size() > 2) { + moduleName = colonSplit[0]; + elementName = colonSplit[1]; + } else { + elementName = colonSplit[0]; + } + funcName = colonSplit.last(); + + QDocDatabase* database = QDocDatabase::qdocDB(); + + auto *aggregate = database->findQmlTypeInPrimaryTree(moduleName, elementName); + // Note: Constructing a QmlType node by default, as opposed to QmlValueType. + // This may lead to unexpected behavior if documenting \qmlvaluetype's methods + // before the type itself. + if (!aggregate) { + aggregate = new QmlTypeNode(database->primaryTreeRoot(), elementName, Node::QmlType); + aggregate->setLocation(location); + if (!moduleName.isEmpty()) + database->addToQmlModule(moduleName, aggregate); + } + + QString params; + QStringList leftParenSplit = funcArg.split('('); + if (leftParenSplit.size() > 1) { + QStringList rightParenSplit = leftParenSplit[1].split(')'); + if (!rightParenSplit.empty()) + params = rightParenSplit[0]; + } + + FunctionNode::Metaness metaness = FunctionNode::getMetanessFromTopic(topic); + bool attached = topic.contains(QLatin1String("attached")); + auto *fn = new FunctionNode(metaness, aggregate, funcName, attached); + fn->setAccess(Access::Public); + fn->setLocation(location); + fn->setReturnType(returnType); + fn->setParameters(params); + return fn; +} + +/*! + Parse the macro arguments in \a macroArg ad hoc, without using + any actual parser. If successful, return a pointer to the new + FunctionNode for the macro. Otherwise return null. \a location + is used for reporting errors. + */ +FunctionNode *CppCodeParser::parseMacroArg(const Location &location, const QString ¯oArg) +{ + QDocDatabase* database = QDocDatabase::qdocDB(); + + QStringList leftParenSplit = macroArg.split('('); + if (leftParenSplit.isEmpty()) + return nullptr; + QString macroName; + FunctionNode *oldMacroNode = nullptr; + QStringList blankSplit = leftParenSplit[0].split(' '); + if (!blankSplit.empty()) { + macroName = blankSplit.last(); + oldMacroNode = database->findMacroNode(macroName); + } + QString returnType; + if (blankSplit.size() > 1) { + blankSplit.removeLast(); + returnType = blankSplit.join(' '); + } + QString params; + if (leftParenSplit.size() > 1) { + const QString &afterParen = leftParenSplit.at(1); + qsizetype rightParen = afterParen.indexOf(')'); + if (rightParen >= 0) + params = afterParen.left(rightParen); + } + int i = 0; + while (i < macroName.size() && !macroName.at(i).isLetter()) + i++; + if (i > 0) { + returnType += QChar(' ') + macroName.left(i); + macroName = macroName.mid(i); + } + FunctionNode::Metaness metaness = FunctionNode::MacroWithParams; + if (params.isEmpty()) + metaness = FunctionNode::MacroWithoutParams; + auto *macro = new FunctionNode(metaness, database->primaryTreeRoot(), macroName); + macro->setAccess(Access::Public); + macro->setLocation(location); + macro->setReturnType(returnType); + macro->setParameters(params); + if (oldMacroNode && macro->parent() == oldMacroNode->parent() + && compare(macro, oldMacroNode) == 0) { + location.warning(QStringLiteral("\\macro %1 documented more than once") + .arg(macroArg), QStringLiteral("also seen here: %1") + .arg(oldMacroNode->doc().location().toString())); + } + return macro; +} + +void CppCodeParser::setExampleFileLists(ExampleNode *en) +{ + Config &config = Config::instance(); + QString fullPath = config.getExampleProjectFile(en->name()); + if (fullPath.isEmpty()) { + QString details = QLatin1String("Example directories: ") + + config.getCanonicalPathList(CONFIG_EXAMPLEDIRS).join(QLatin1Char(' ')); + en->location().warning( + QStringLiteral("Cannot find project file for example '%1'").arg(en->name()), + details); + return; + } + + QDir exampleDir(QFileInfo(fullPath).dir()); + + const auto& [excludeDirs, excludeFiles] = config.getExcludedPaths(); + + QStringList exampleFiles = Config::getFilesHere(exampleDir.path(), m_exampleNameFilter, + Location(), excludeDirs, excludeFiles); + // Search for all image files under the example project, excluding doc/images directory. + QSet<QString> excludeDocDirs(excludeDirs); + excludeDocDirs.insert(exampleDir.path() + QLatin1String("/doc/images")); + QStringList imageFiles = Config::getFilesHere(exampleDir.path(), m_exampleImageFilter, + Location(), excludeDocDirs, excludeFiles); + if (!exampleFiles.isEmpty()) { + // move main.cpp to the end, if it exists + QString mainCpp; + + const auto isGeneratedOrMainCpp = [&mainCpp](const QString &fileName) { + if (fileName.endsWith("/main.cpp")) { + if (mainCpp.isEmpty()) + mainCpp = fileName; + return true; + } + return fileName.contains("/qrc_") || fileName.contains("/moc_") + || fileName.contains("/ui_"); + }; + + exampleFiles.erase( + std::remove_if(exampleFiles.begin(), exampleFiles.end(), isGeneratedOrMainCpp), + exampleFiles.end()); + + if (!mainCpp.isEmpty()) + exampleFiles.append(mainCpp); + + // Add any resource and project files + exampleFiles += Config::getFilesHere(exampleDir.path(), + QLatin1String("*.qrc *.pro *.qmlproject *.pyproject CMakeLists.txt qmldir"), + Location(), excludeDirs, excludeFiles); + } + + const qsizetype pathLen = exampleDir.path().size() - en->name().size(); + for (auto &file : exampleFiles) + file = file.mid(pathLen); + for (auto &file : imageFiles) + file = file.mid(pathLen); + + en->setFiles(exampleFiles, fullPath.mid(pathLen)); + en->setImages(imageFiles); +} + +/*! + returns true if \a t is \e {qmlsignal}, \e {qmlmethod}, + \e {qmlattachedsignal}, or \e {qmlattachedmethod}. + */ +bool CppCodeParser::isQMLMethodTopic(const QString &t) +{ + return (t == COMMAND_QMLSIGNAL || t == COMMAND_QMLMETHOD || t == COMMAND_QMLATTACHEDSIGNAL + || t == COMMAND_QMLATTACHEDMETHOD); +} + +/*! + Returns true if \a t is \e {qmlproperty}, \e {qmlpropertygroup}, + or \e {qmlattachedproperty}. + */ +bool CppCodeParser::isQMLPropertyTopic(const QString &t) +{ + return (t == COMMAND_QMLPROPERTY || t == COMMAND_QMLATTACHEDPROPERTY); +} + +std::pair<std::vector<TiedDocumentation>, std::vector<FnMatchError>> +CppCodeParser::processTopicArgs(const UntiedDocumentation &untied) +{ + const Doc &doc = untied.documentation; + + if (doc.topicsUsed().isEmpty()) + return {}; + + QDocDatabase *database = QDocDatabase::qdocDB(); + + const QString topic = doc.topicsUsed().first().m_topic; + + std::vector<TiedDocumentation> tied{}; + std::vector<FnMatchError> errors{}; + + if (isQMLPropertyTopic(topic)) { + auto tied_qml = processQmlProperties(untied); + tied.insert(tied.end(), tied_qml.begin(), tied_qml.end()); + } else { + ArgList args = doc.metaCommandArgs(topic); + Node *node = nullptr; + if (args.size() == 1) { + if (topic == COMMAND_FN) { + if (Config::instance().showInternal() || !doc.isInternal()) { + auto result = fn_parser(doc.location(), args[0].first, args[0].second, untied.context); + if (auto *error = std::get_if<FnMatchError>(&result)) + errors.emplace_back(*error); + else + node = std::get<Node*>(result); + } + } else if (topic == COMMAND_MACRO) { + node = parseMacroArg(doc.location(), args[0].first); + } else if (isQMLMethodTopic(topic)) { + node = parseOtherFuncArg(topic, doc.location(), args[0].first); + } else if (topic == COMMAND_DONTDOCUMENT) { + database->primaryTree()->addToDontDocumentMap(args[0].first); + } else { + node = processTopicCommand(doc, topic, args[0]); + } + if (node != nullptr) { + tied.emplace_back(TiedDocumentation{doc, node}); + } + } else if (args.size() > 1) { + QList<SharedCommentNode *> sharedCommentNodes; + for (const auto &arg : std::as_const(args)) { + node = nullptr; + if (topic == COMMAND_FN) { + if (Config::instance().showInternal() || !doc.isInternal()) { + auto result = fn_parser(doc.location(), arg.first, arg.second, untied.context); + if (auto *error = std::get_if<FnMatchError>(&result)) + errors.emplace_back(*error); + else + node = std::get<Node*>(result); + } + } else if (topic == COMMAND_MACRO) { + node = parseMacroArg(doc.location(), arg.first); + } else if (isQMLMethodTopic(topic)) { + node = parseOtherFuncArg(topic, doc.location(), arg.first); + } else { + node = processTopicCommand(doc, topic, arg); + } + if (node != nullptr) { + bool found = false; + for (SharedCommentNode *scn : sharedCommentNodes) { + if (scn->parent() == node->parent()) { + scn->append(node); + found = true; + break; + } + } + if (!found) { + auto *scn = new SharedCommentNode(node); + sharedCommentNodes.append(scn); + tied.emplace_back(TiedDocumentation{doc, scn}); + } + } + } + for (auto *scn : sharedCommentNodes) + scn->sort(); + } + } + return std::make_pair(tied, errors); +} + +/*! + For each node that is part of C++ API and produces a documentation + page, this function ensures that the node belongs to a module. + */ +static void checkModuleInclusion(Node *n) +{ + if (n->physicalModuleName().isEmpty()) { + if (n->isInAPI() && !n->name().isEmpty()) { + switch (n->nodeType()) { + case Node::Class: + case Node::Struct: + case Node::Union: + case Node::Namespace: + case Node::HeaderFile: + break; + default: + return; + } + n->setPhysicalModuleName(Generator::defaultModuleName()); + QDocDatabase::qdocDB()->addToModule(Generator::defaultModuleName(), n); + n->doc().location().warning( + QStringLiteral("Documentation for %1 '%2' has no \\inmodule command; " + "using project name by default: %3") + .arg(Node::nodeTypeString(n->nodeType()), n->name(), + n->physicalModuleName())); + } + } +} + +void CppCodeParser::processMetaCommands(const std::vector<TiedDocumentation> &tied) +{ + for (auto [doc, node] : tied) { + processMetaCommands(doc, node); + node->setDoc(doc); + checkModuleInclusion(node); + if (node->isAggregate()) { + auto *aggregate = static_cast<Aggregate *>(node); + + if (!aggregate->includeFile()) { + Aggregate *parent = aggregate; + while (parent->physicalModuleName().isEmpty() && (parent->parent() != nullptr)) + parent = parent->parent(); + + if (parent == aggregate) + // TODO: Understand if the name can be empty. + // In theory it should not be possible as + // there would be no aggregate to refer to + // such that this code is never reached. + // + // If the name can be empty, this would + // endanger users of the include file down the + // line, forcing them to ensure that, further + // to there being an actual include file, that + // include file is not an empty string, such + // that we would require a different way to + // generate the include file here. + aggregate->setIncludeFile(aggregate->name()); + else if (aggregate->includeFile()) + aggregate->setIncludeFile(*parent->includeFile()); + } + } + } +} + +void CppCodeParser::processQmlNativeTypeCommand(Node *node, const QString &arg, const Location &location) +{ + Q_ASSERT(node); + if (!node->isQmlNode()) { + location.warning( + QStringLiteral("Command '\\%1' is only meaningful in '\\%2'") + .arg(COMMAND_QMLNATIVETYPE, COMMAND_QMLTYPE)); + return; + } + + auto qmlNode = static_cast<QmlTypeNode *>(node); + + QDocDatabase *database = QDocDatabase::qdocDB(); + auto classNode = database->findClassNode(arg.split(u"::"_s)); + + if (!classNode) { + if (m_showLinkErrors) { + location.warning( + QStringLiteral("C++ class %2 not found: \\%1 %2") + .arg(COMMAND_QMLNATIVETYPE, arg)); + } + return; + } + + if (qmlNode->classNode()) { + location.warning( + QStringLiteral("QML type %1 documented with %2 as its native type. Replacing %2 with %3") + .arg(qmlNode->name(), qmlNode->classNode()->name(), arg)); + } + + qmlNode->setClassNode(classNode); + classNode->insertQmlNativeType(qmlNode); +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/cppcodeparser.h b/src/qdoc/qdoc/src/qdoc/cppcodeparser.h new file mode 100644 index 000000000..32e11d05b --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/cppcodeparser.h @@ -0,0 +1,111 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef CPPCODEPARSER_H +#define CPPCODEPARSER_H + +#include "clangcodeparser.h" +#include "codeparser.h" +#include "parsererror.h" +#include "utilities.h" + +QT_BEGIN_NAMESPACE + +class ClassNode; +class ExampleNode; +class FunctionNode; +class Aggregate; + +class CppCodeParser +{ +public: + static inline const QSet<QString> topic_commands{ + COMMAND_CLASS, COMMAND_DONTDOCUMENT, COMMAND_ENUM, COMMAND_EXAMPLE, + COMMAND_EXTERNALPAGE, COMMAND_FN, COMMAND_GROUP, COMMAND_HEADERFILE, + COMMAND_MACRO, COMMAND_MODULE, COMMAND_NAMESPACE, COMMAND_PAGE, + COMMAND_PROPERTY, COMMAND_TYPEALIAS, COMMAND_TYPEDEF, COMMAND_VARIABLE, + COMMAND_QMLTYPE, COMMAND_QMLPROPERTY, COMMAND_QMLPROPERTYGROUP, + COMMAND_QMLATTACHEDPROPERTY, COMMAND_QMLSIGNAL, COMMAND_QMLATTACHEDSIGNAL, + COMMAND_QMLMETHOD, COMMAND_QMLATTACHEDMETHOD, COMMAND_QMLVALUETYPE, COMMAND_QMLBASICTYPE, + COMMAND_QMLMODULE, COMMAND_STRUCT, COMMAND_UNION, + }; + + static inline const QSet<QString> meta_commands = QSet<QString>(CodeParser::common_meta_commands) + << COMMAND_COMPARES << COMMAND_COMPARESWITH << COMMAND_INHEADERFILE + << COMMAND_NEXTPAGE << COMMAND_OVERLOAD << COMMAND_PREVIOUSPAGE + << COMMAND_QMLINSTANTIATES << COMMAND_QMLNATIVETYPE << COMMAND_REIMP << COMMAND_RELATES; + +public: + explicit CppCodeParser(FnCommandParser&& parser); + + FunctionNode *parseMacroArg(const Location &location, const QString ¯oArg); + FunctionNode *parseOtherFuncArg(const QString &topic, const Location &location, + const QString &funcArg); + static bool isQMLMethodTopic(const QString &t); + static bool isQMLPropertyTopic(const QString &t); + + std::pair<std::vector<TiedDocumentation>, std::vector<FnMatchError>> + processTopicArgs(const UntiedDocumentation &untied); + + void processMetaCommand(const Doc &doc, const QString &command, const ArgPair &argLocPair, + Node *node); + void processMetaCommands(const Doc &doc, Node *node); + void processMetaCommands(const std::vector<TiedDocumentation> &tied); + +protected: + virtual Node *processTopicCommand(const Doc &doc, const QString &command, + const ArgPair &arg); + std::vector<TiedDocumentation> processQmlProperties(const UntiedDocumentation& untied); + bool splitQmlPropertyArg(const QString &arg, QString &type, QString &module, QString &element, + QString &name, const Location &location); + +private: + void setExampleFileLists(ExampleNode *en); + static void processComparesCommand(Node *node, const QString &arg, const Location &loc); + void processQmlNativeTypeCommand(Node *node, const QString &arg, const Location &loc); + +private: + FnCommandParser fn_parser; + QString m_exampleNameFilter; + QString m_exampleImageFilter; + bool m_showLinkErrors { false }; +}; + +/*! + * \internal + * \brief Checks if there are too many topic commands in \a doc. + * + * This method compares the commands used in \a doc with the set of topic + * commands. If zero or one topic command is found, or if all found topic + * commands are {\\qml*}-commands, the method returns \c false. + * + * If more than one topic command is found, QDoc issues a warning and the list + * of topic commands used in \a doc, and the method returns \c true. + */ +[[nodiscard]] inline bool hasTooManyTopics(const Doc &doc) +{ + const QSet<QString> topicCommandsUsed = CppCodeParser::topic_commands & doc.metaCommandsUsed(); + + if (topicCommandsUsed.empty() || topicCommandsUsed.size() == 1) + return false; + if (std::all_of(topicCommandsUsed.cbegin(), topicCommandsUsed.cend(), + [](const auto &cmd) { return cmd.startsWith(QLatin1String("qml")); })) + return false; + + const QStringList commands = topicCommandsUsed.values(); + const QString topicCommands{ std::accumulate( + commands.cbegin(), commands.cend(), QString{}, + [index = qsizetype{ 0 }, numberOfCommands = commands.size()]( + const QString &accumulator, const QString &topic) mutable -> QString { + return accumulator + QLatin1String("\\") + topic + + Utilities::separator(index++, numberOfCommands); + }) }; + + doc.location().warning( + QStringLiteral("Multiple topic commands found in comment: %1").arg(topicCommands)); + return true; +} + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/doc.cpp b/src/qdoc/qdoc/src/qdoc/doc.cpp new file mode 100644 index 000000000..4a2aa72fd --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/doc.cpp @@ -0,0 +1,426 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "doc.h" + +#include "atom.h" +#include "config.h" +#include "codemarker.h" +#include "docparser.h" +#include "docprivate.h" +#include "generator.h" +#include "qmltypenode.h" +#include "quoter.h" +#include "text.h" +#include "utilities.h" + +#include <qcryptographichash.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +DocUtilities &Doc::m_utilities = DocUtilities::instance(); + +/*! + \typedef ArgList + \relates Doc + + A list of metacommand arguments that appear in a Doc. Each entry + in the list is a <QString, QString> pair (ArgPair): + + \list + \li \c {ArgPair.first} - arguments passed to the command. + \li \c {ArgPair.second} - optional argument string passed + within brackets immediately following the command. + \endlist +*/ + +/*! + Parse the qdoc comment \a source. Build up a list of all the topic + commands found including their arguments. This constructor is used + when there can be more than one topic command in theqdoc comment. + Normally, there is only one topic command in a qdoc comment, but in + QML documentation, there is the case where the qdoc \e{qmlproperty} + command can appear multiple times in a qdoc comment. + */ +Doc::Doc(const Location &start_loc, const Location &end_loc, const QString &source, + const QSet<QString> &metaCommandSet, const QSet<QString> &topics) +{ + m_priv = new DocPrivate(start_loc, end_loc, source); + DocParser parser; + parser.parse(source, m_priv, metaCommandSet, topics); + + if (Config::instance().getAtomsDump()) { + start_loc.information(u"==== Atoms Structure for block comment starting at %1 ===="_s.arg( + start_loc.toString())); + body().dump(); + end_loc.information( + u"==== Ending atoms Structure for block comment ending at %1 ===="_s.arg( + end_loc.toString())); + } +} + +Doc::Doc(const Doc &doc) : m_priv(nullptr) +{ + operator=(doc); +} + +Doc::~Doc() +{ + if (m_priv && m_priv->deref()) + delete m_priv; +} + +Doc &Doc::operator=(const Doc &doc) +{ + if (&doc == this) + return *this; + if (doc.m_priv) + doc.m_priv->ref(); + if (m_priv && m_priv->deref()) + delete m_priv; + m_priv = doc.m_priv; + return *this; +} + +/*! + Returns the starting location of a qdoc comment. + */ +const Location &Doc::location() const +{ + static const Location dummy; + return m_priv == nullptr ? dummy : m_priv->m_start_loc; +} + +/*! + Returns the starting location of a qdoc comment. + */ +const Location &Doc::startLocation() const +{ + return location(); +} + +const QString &Doc::source() const +{ + static QString null; + return m_priv == nullptr ? null : m_priv->m_src; +} + +bool Doc::isEmpty() const +{ + return m_priv == nullptr || m_priv->m_src.isEmpty(); +} + +const Text &Doc::body() const +{ + static const Text dummy; + return m_priv == nullptr ? dummy : m_priv->m_text; +} + +Text Doc::briefText(bool inclusive) const +{ + return body().subText(Atom::BriefLeft, Atom::BriefRight, nullptr, inclusive); +} + +Text Doc::trimmedBriefText(const QString &className) const +{ + QString classNameOnly = className; + if (className.contains("::")) + classNameOnly = className.split("::").last(); + + Text originalText = briefText(); + Text resultText; + const Atom *atom = originalText.firstAtom(); + if (atom) { + QString briefStr; + QString whats; + /* + This code is really ugly. The entire \brief business + should be rethought. + */ + while (atom) { + if (atom->type() == Atom::AutoLink || atom->type() == Atom::String) { + briefStr += atom->string(); + } else if (atom->type() == Atom::C) { + briefStr += Generator::plainCode(atom->string()); + } + atom = atom->next(); + } + + QStringList w = briefStr.split(QLatin1Char(' ')); + if (!w.isEmpty() && w.first() == "Returns") { + } else { + if (!w.isEmpty() && w.first() == "The") + w.removeFirst(); + + if (!w.isEmpty() && (w.first() == className || w.first() == classNameOnly)) + w.removeFirst(); + + if (!w.isEmpty() + && ((w.first() == "class") || (w.first() == "function") || (w.first() == "macro") + || (w.first() == "widget") || (w.first() == "namespace") + || (w.first() == "header"))) + w.removeFirst(); + + if (!w.isEmpty() && (w.first() == "is" || w.first() == "provides")) + w.removeFirst(); + + if (!w.isEmpty() && (w.first() == "a" || w.first() == "an")) + w.removeFirst(); + } + + whats = w.join(' '); + + if (whats.endsWith(QLatin1Char('.'))) + whats.truncate(whats.size() - 1); + + if (!whats.isEmpty()) + whats[0] = whats[0].toUpper(); + + // ### move this once \brief is abolished for properties + resultText << whats; + } + return resultText; +} + +Text Doc::legaleseText() const +{ + if (m_priv == nullptr || !m_priv->m_hasLegalese) + return Text(); + else + return body().subText(Atom::LegaleseLeft, Atom::LegaleseRight); +} + +QSet<QString> Doc::parameterNames() const +{ + return m_priv == nullptr ? QSet<QString>() : m_priv->m_params; +} + +QStringList Doc::enumItemNames() const +{ + return m_priv == nullptr ? QStringList() : m_priv->m_enumItemList; +} + +QStringList Doc::omitEnumItemNames() const +{ + return m_priv == nullptr ? QStringList() : m_priv->m_omitEnumItemList; +} + +QSet<QString> Doc::metaCommandsUsed() const +{ + return m_priv == nullptr ? QSet<QString>() : m_priv->m_metacommandsUsed; +} + +/*! + Returns true if the set of metacommands used in the doc + comment contains \e {internal}. + */ +bool Doc::isInternal() const +{ + return metaCommandsUsed().contains(QLatin1String("internal")); +} + +/*! + Returns true if the set of metacommands used in the doc + comment contains \e {reimp}. + */ +bool Doc::isMarkedReimp() const +{ + return metaCommandsUsed().contains(QLatin1String("reimp")); +} + +/*! + Returns a reference to the list of topic commands used in the + current qdoc comment. Normally there is only one, but there + can be multiple \e{qmlproperty} commands, for example. + */ +TopicList Doc::topicsUsed() const +{ + return m_priv == nullptr ? TopicList() : m_priv->m_topics; +} + +ArgList Doc::metaCommandArgs(const QString &metacommand) const +{ + return m_priv == nullptr ? ArgList() : m_priv->m_metaCommandMap.value(metacommand); +} + +QList<Text> Doc::alsoList() const +{ + return m_priv == nullptr ? QList<Text>() : m_priv->m_alsoList; +} + +bool Doc::hasTableOfContents() const +{ + return m_priv && m_priv->extra && !m_priv->extra->m_tableOfContents.isEmpty(); +} + +bool Doc::hasKeywords() const +{ + return m_priv && m_priv->extra && !m_priv->extra->m_keywords.isEmpty(); +} + +bool Doc::hasTargets() const +{ + return m_priv && m_priv->extra && !m_priv->extra->m_targets.isEmpty(); +} + +const QList<Atom *> &Doc::tableOfContents() const +{ + m_priv->constructExtra(); + return m_priv->extra->m_tableOfContents; +} + +const QList<int> &Doc::tableOfContentsLevels() const +{ + m_priv->constructExtra(); + return m_priv->extra->m_tableOfContentsLevels; +} + +const QList<Atom *> &Doc::keywords() const +{ + m_priv->constructExtra(); + return m_priv->extra->m_keywords; +} + +const QList<Atom *> &Doc::targets() const +{ + m_priv->constructExtra(); + return m_priv->extra->m_targets; +} + +QStringMultiMap *Doc::metaTagMap() const +{ + return m_priv && m_priv->extra ? &m_priv->extra->m_metaMap : nullptr; +} + +QMultiMap<ComparisonCategory, Text> *Doc::comparesWithMap() const +{ + return m_priv && m_priv->extra ? &m_priv->extra->m_comparesWithMap : nullptr; +} + +void Doc::constructExtra() const +{ + if (m_priv) + m_priv->constructExtra(); +} + +void Doc::initialize(FileResolver& file_resolver) +{ + Config &config = Config::instance(); + DocParser::initialize(config, file_resolver); + + const auto &configMacros = config.subVars(CONFIG_MACRO); + for (const auto ¯oName : configMacros) { + QString macroDotName = CONFIG_MACRO + Config::dot + macroName; + Macro macro; + macro.numParams = -1; + const auto ¯oConfigVar = config.get(macroDotName); + macro.m_defaultDef = macroConfigVar.asString(); + if (!macro.m_defaultDef.isEmpty()) { + macro.m_defaultDefLocation = macroConfigVar.location(); + macro.numParams = Config::numParams(macro.m_defaultDef); + } + bool silent = false; + + const auto ¯oDotNames = config.subVars(macroDotName); + for (const auto &f : macroDotNames) { + const auto ¯oSubVar = config.get(macroDotName + Config::dot + f); + QString def{macroSubVar.asString()}; + if (!def.isEmpty()) { + macro.m_otherDefs.insert(f, def); + int m = Config::numParams(def); + if (macro.numParams == -1) + macro.numParams = m; + // .match definition is a regular expression that contains no params + else if (macro.numParams != m && f != QLatin1String("match")) { + if (!silent) { + QString other = QStringLiteral("default"); + if (macro.m_defaultDef.isEmpty()) + other = macro.m_otherDefs.constBegin().key(); + macroSubVar.location().warning( + QStringLiteral("Macro '\\%1' takes inconsistent number of " + "arguments (%2 %3, %4 %5)") + .arg(macroName, f, QString::number(m), other, + QString::number(macro.numParams))); + silent = true; + } + if (macro.numParams < m) + macro.numParams = m; + } + } + } + if (macro.numParams != -1) + m_utilities.macroHash.insert(macroName, macro); + } +} + +/*! + All the heap allocated variables are deleted. + */ +void Doc::terminate() +{ + m_utilities.cmdHash.clear(); + m_utilities.macroHash.clear(); +} + +/*! + Trims the deadwood out of \a str. i.e., this function + cleans up \a str. + */ +void Doc::trimCStyleComment(Location &location, QString &str) +{ + QString cleaned; + Location m = location; + bool metAsterColumn = true; + int asterColumn = location.columnNo() + 1; + int i; + + for (i = 0; i < str.size(); ++i) { + if (m.columnNo() == asterColumn) { + if (str[i] != '*') + break; + cleaned += ' '; + metAsterColumn = true; + } else { + if (str[i] == '\n') { + if (!metAsterColumn) + break; + metAsterColumn = false; + } + cleaned += str[i]; + } + m.advance(str[i]); + } + if (cleaned.size() == str.size()) + str = cleaned; + + for (int i = 0; i < 3; ++i) + location.advance(str[i]); + str = str.mid(3, str.size() - 5); +} + +void Doc::quoteFromFile(const Location &location, Quoter "er, ResolvedFile resolved_file) +{ + // TODO: quoteFromFile should not care about modifying a stateful + // quoter from the outside, instead, it should produce a quoter + // that allows the caller to retrieve the required information + // about the quoted file. + // + // When changing the way in which quoting works, this kind of + // spread resposability should be removed, together with quoteFromFile. + quoter.reset(); + + QString code; + { + QFile input_file{resolved_file.get_path()}; + if (!input_file.open(QFile::ReadOnly)) + return; + code = DocParser::untabifyEtc(QTextStream{&input_file}.readAll()); + } + + CodeMarker *marker = CodeMarker::markerForFileName(resolved_file.get_path()); + quoter.quoteFromFile(resolved_file.get_path(), code, marker->markedUpCode(code, nullptr, location)); +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/doc.h b/src/qdoc/qdoc/src/qdoc/doc.h new file mode 100644 index 000000000..49a9f7947 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/doc.h @@ -0,0 +1,93 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef DOC_H +#define DOC_H + +#include "location.h" +#include "comparisoncategory.h" +#include "docutilities.h" +#include "topic.h" + +#include "filesystem/fileresolver.h" +#include "boundaries/filesystem/resolvedfile.h" + +#include <QtCore/qmap.h> +#include <QtCore/qset.h> +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +class Atom; +class DocPrivate; +class Quoter; +class Text; + +typedef std::pair<QString, QString> ArgPair; +typedef QList<ArgPair> ArgList; +typedef QMultiMap<QString, QString> QStringMultiMap; + +class Doc +{ +public: + // the order is important + enum Sections { + NoSection = -1, + Section1 = 1, + Section2 = 2, + Section3 = 3, + Section4 = 4 + }; + + Doc() = default; + Doc(const Location &start_loc, const Location &end_loc, const QString &source, + const QSet<QString> &metaCommandSet, const QSet<QString> &topics); + Doc(const Doc &doc); + ~Doc(); + + Doc &operator=(const Doc &doc); + + [[nodiscard]] const Location &location() const; + [[nodiscard]] const Location &startLocation() const; + [[nodiscard]] bool isEmpty() const; + [[nodiscard]] const QString &source() const; + [[nodiscard]] const Text &body() const; + [[nodiscard]] Text briefText(bool inclusive = false) const; + [[nodiscard]] Text trimmedBriefText(const QString &className) const; + [[nodiscard]] Text legaleseText() const; + [[nodiscard]] QSet<QString> parameterNames() const; + [[nodiscard]] QStringList enumItemNames() const; + [[nodiscard]] QStringList omitEnumItemNames() const; + [[nodiscard]] QSet<QString> metaCommandsUsed() const; + [[nodiscard]] TopicList topicsUsed() const; + [[nodiscard]] ArgList metaCommandArgs(const QString &metaCommand) const; + [[nodiscard]] QList<Text> alsoList() const; + [[nodiscard]] bool hasTableOfContents() const; + [[nodiscard]] bool hasKeywords() const; + [[nodiscard]] bool hasTargets() const; + [[nodiscard]] bool isInternal() const; + [[nodiscard]] bool isMarkedReimp() const; + [[nodiscard]] const QList<Atom *> &tableOfContents() const; + [[nodiscard]] const QList<int> &tableOfContentsLevels() const; + [[nodiscard]] const QList<Atom *> &keywords() const; + [[nodiscard]] const QList<Atom *> &targets() const; + [[nodiscard]] QStringMultiMap *metaTagMap() const; + [[nodiscard]] QMultiMap<ComparisonCategory, Text> *comparesWithMap() const; + void constructExtra() const; + + static void initialize(FileResolver& file_resolver); + static void terminate(); + static void trimCStyleComment(Location &location, QString &str); + static void quoteFromFile(const Location &location, Quoter "er, + ResolvedFile resolved_file); + +private: + DocPrivate *m_priv { nullptr }; + static DocUtilities &m_utilities; +}; +Q_DECLARE_TYPEINFO(Doc, Q_RELOCATABLE_TYPE); +typedef QList<Doc> DocList; + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/docbookgenerator.cpp b/src/qdoc/qdoc/src/qdoc/docbookgenerator.cpp new file mode 100644 index 000000000..6ac83ff13 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/docbookgenerator.cpp @@ -0,0 +1,4771 @@ +// Copyright (C) 2019 Thibaut Cuvelier +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "docbookgenerator.h" + +#include "access.h" +#include "aggregate.h" +#include "classnode.h" +#include "codemarker.h" +#include "collectionnode.h" +#include "comparisoncategory.h" +#include "config.h" +#include "enumnode.h" +#include "examplenode.h" +#include "functionnode.h" +#include "generator.h" +#include "node.h" +#include "propertynode.h" +#include "quoter.h" +#include "qdocdatabase.h" +#include "qmlpropertynode.h" +#include "sharedcommentnode.h" +#include "typedefnode.h" +#include "variablenode.h" + +#include <QtCore/qlist.h> +#include <QtCore/qmap.h> +#include <QtCore/quuid.h> +#include <QtCore/qurl.h> +#include <QtCore/qregularexpression.h> +#include <QtCore/qversionnumber.h> + +#include <cctype> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +static const char dbNamespace[] = "http://docbook.org/ns/docbook"; +static const char xlinkNamespace[] = "http://www.w3.org/1999/xlink"; +static const char itsNamespace[] = "http://www.w3.org/2005/11/its"; + +DocBookGenerator::DocBookGenerator(FileResolver& file_resolver) : XmlGenerator(file_resolver) {} + +inline void DocBookGenerator::newLine() +{ + m_writer->writeCharacters("\n"); +} + +void DocBookGenerator::writeXmlId(const QString &id) +{ + if (id.isEmpty()) + return; + + m_writer->writeAttribute("xml:id", registerRef(id, true)); +} + +void DocBookGenerator::writeXmlId(const Node *node) +{ + if (!node) + return; + + // Specifically for nodes, do not use the same code path as for QString + // inputs, as refForNode calls registerRef in all cases. Calling + // registerRef a second time adds a character to "disambiguate" the two IDs + // (the one returned by refForNode, then the one that is written as + // xml:id). + QString id = Generator::cleanRef(refForNode(node), true); + if (!id.isEmpty()) + m_writer->writeAttribute("xml:id", id); +} + +void DocBookGenerator::startSectionBegin(const QString &id) +{ + m_hasSection = true; + + m_writer->writeStartElement(dbNamespace, "section"); + writeXmlId(id); + newLine(); + m_writer->writeStartElement(dbNamespace, "title"); +} + +void DocBookGenerator::startSectionBegin(const Node *node) +{ + m_writer->writeStartElement(dbNamespace, "section"); + writeXmlId(node); + newLine(); + m_writer->writeStartElement(dbNamespace, "title"); +} + +void DocBookGenerator::startSectionEnd() +{ + m_writer->writeEndElement(); // title + newLine(); +} + +void DocBookGenerator::startSection(const QString &id, const QString &title) +{ + startSectionBegin(id); + m_writer->writeCharacters(title); + startSectionEnd(); +} + +void DocBookGenerator::startSection(const Node *node, const QString &title) +{ + startSectionBegin(node); + m_writer->writeCharacters(title); + startSectionEnd(); +} + +void DocBookGenerator::startSection(const QString &title) +{ + // No xml:id given: down the calls, "" is interpreted as "no ID". + startSection("", title); +} + +void DocBookGenerator::endSection() +{ + m_writer->writeEndElement(); // section + newLine(); +} + +void DocBookGenerator::writeAnchor(const QString &id) +{ + if (id.isEmpty()) + return; + + m_writer->writeEmptyElement(dbNamespace, "anchor"); + writeXmlId(id); + newLine(); +} + +/*! + Initializes the DocBook output generator's data structures + from the configuration (Config). + */ +void DocBookGenerator::initializeGenerator() +{ + // Excerpts from HtmlGenerator::initializeGenerator. + Generator::initializeGenerator(); + m_config = &Config::instance(); + + m_project = m_config->get(CONFIG_PROJECT).asString(); + + m_projectDescription = m_config->get(CONFIG_DESCRIPTION).asString(); + if (m_projectDescription.isEmpty() && !m_project.isEmpty()) + m_projectDescription = m_project + QLatin1String(" Reference Documentation"); + + m_naturalLanguage = m_config->get(CONFIG_NATURALLANGUAGE).asString(); + if (m_naturalLanguage.isEmpty()) + m_naturalLanguage = QLatin1String("en"); + + m_buildVersion = m_config->get(CONFIG_BUILDVERSION).asString(); + m_useDocBook52 = m_config->get(CONFIG_DOCBOOKEXTENSIONS).asBool() || + m_config->get(format() + Config::dot + "usedocbookextensions").asBool(); + m_useITS = m_config->get(format() + Config::dot + "its").asBool(); +} + +QString DocBookGenerator::format() +{ + return "DocBook"; +} + +/*! + Returns "xml" for this subclass of Generator. + */ +QString DocBookGenerator::fileExtension() const +{ + return "xml"; +} + +/*! + Generate the documentation for \a relative. i.e. \a relative + is the node that represents the entity where a qdoc comment + was found, and \a text represents the qdoc comment. + */ +bool DocBookGenerator::generateText(const Text &text, const Node *relative) +{ + // From Generator::generateText. + if (!text.firstAtom()) + return false; + + int numAtoms = 0; + initializeTextOutput(); + generateAtomList(text.firstAtom(), relative, nullptr, true, numAtoms); + closeTextSections(); + return true; +} + +QString removeCodeMarkers(const QString& code) { + QString rewritten = code; + static const QRegularExpression re("(<@[^>&]*>)|(<\\/@[^&>]*>)"); + rewritten.replace(re, ""); + return rewritten; +} + +/*! + Generate DocBook from an instance of Atom. + */ +qsizetype DocBookGenerator::generateAtom(const Atom *atom, const Node *relative, CodeMarker*) +{ + Q_ASSERT(m_writer); + // From HtmlGenerator::generateAtom, without warning generation. + int idx = 0; + int skipAhead = 0; + Node::Genus genus = Node::DontCare; + + switch (atom->type()) { + case Atom::AutoLink: + // Allow auto-linking to nodes in API reference + genus = Node::API; + Q_FALLTHROUGH(); + case Atom::NavAutoLink: + if (!m_inLink && !m_inContents && !m_inSectionHeading) { + const Node *node = nullptr; + QString link = getAutoLink(atom, relative, &node, genus); + if (!link.isEmpty() && node && node->isDeprecated() + && relative->parent() != node && !relative->isDeprecated()) { + link.clear(); + } + if (link.isEmpty()) { + m_writer->writeCharacters(atom->string()); + } else { + beginLink(link, node, relative); + generateLink(atom); + endLink(); + } + } else { + m_writer->writeCharacters(atom->string()); + } + break; + case Atom::BaseName: + break; + case Atom::BriefLeft: + if (!hasBrief(relative)) { + skipAhead = skipAtoms(atom, Atom::BriefRight); + break; + } + m_writer->writeStartElement(dbNamespace, "para"); + m_inPara = true; + rewritePropertyBrief(atom, relative); + break; + case Atom::BriefRight: + if (hasBrief(relative)) { + m_writer->writeEndElement(); // para + m_inPara = false; + newLine(); + } + break; + case Atom::C: + // This may at one time have been used to mark up C++ code but it is + // now widely used to write teletype text. As a result, text marked + // with the \c command is not passed to a code marker. + if (m_inTeletype) + m_writer->writeCharacters(plainCode(atom->string())); + else + m_writer->writeTextElement(dbNamespace, "code", plainCode(atom->string())); + break; + case Atom::CaptionLeft: + m_writer->writeStartElement(dbNamespace, "title"); + break; + case Atom::CaptionRight: + endLink(); + m_writer->writeEndElement(); // title + newLine(); + break; + case Atom::Qml: + m_writer->writeStartElement(dbNamespace, "programlisting"); + m_writer->writeAttribute("language", "qml"); + if (m_useITS) + m_writer->writeAttribute(itsNamespace, "translate", "no"); + m_writer->writeCharacters(removeCodeMarkers(atom->string())); + m_writer->writeEndElement(); // programlisting + newLine(); + break; + case Atom::Code: + m_writer->writeStartElement(dbNamespace, "programlisting"); + m_writer->writeAttribute("language", "cpp"); + if (m_useITS) + m_writer->writeAttribute(itsNamespace, "translate", "no"); + m_writer->writeCharacters(removeCodeMarkers(atom->string())); + m_writer->writeEndElement(); // programlisting + newLine(); + break; + case Atom::CodeBad: + m_writer->writeStartElement(dbNamespace, "programlisting"); + m_writer->writeAttribute("language", "cpp"); + m_writer->writeAttribute("role", "bad"); + if (m_useITS) + m_writer->writeAttribute(itsNamespace, "translate", "no"); + m_writer->writeCharacters(removeCodeMarkers(atom->string())); + m_writer->writeEndElement(); // programlisting + newLine(); + break; + case Atom::DetailsLeft: + case Atom::DetailsRight: + break; + case Atom::DivLeft: + case Atom::DivRight: + break; + case Atom::FootnoteLeft: + m_writer->writeStartElement(dbNamespace, "footnote"); + newLine(); + m_writer->writeStartElement(dbNamespace, "para"); + m_inPara = true; + break; + case Atom::FootnoteRight: + m_writer->writeEndElement(); // para + m_inPara = false; + newLine(); + m_writer->writeEndElement(); // footnote + break; + case Atom::FormatElse: + case Atom::FormatEndif: + case Atom::FormatIf: + break; + case Atom::FormattingLeft: + if (atom->string() == ATOM_FORMATTING_BOLD) { + m_writer->writeStartElement(dbNamespace, "emphasis"); + m_writer->writeAttribute("role", "bold"); + } else if (atom->string() == ATOM_FORMATTING_ITALIC) { + m_writer->writeStartElement(dbNamespace, "emphasis"); + } else if (atom->string() == ATOM_FORMATTING_UNDERLINE) { + m_writer->writeStartElement(dbNamespace, "emphasis"); + m_writer->writeAttribute("role", "underline"); + } else if (atom->string() == ATOM_FORMATTING_SUBSCRIPT) { + m_writer->writeStartElement(dbNamespace, "subscript"); + } else if (atom->string() == ATOM_FORMATTING_SUPERSCRIPT) { + m_writer->writeStartElement(dbNamespace, "superscript"); + } else if (atom->string() == ATOM_FORMATTING_TELETYPE + || atom->string() == ATOM_FORMATTING_PARAMETER) { + m_writer->writeStartElement(dbNamespace, "code"); + if (m_useITS) + m_writer->writeAttribute(itsNamespace, "translate", "no"); + + if (atom->string() == ATOM_FORMATTING_PARAMETER) + m_writer->writeAttribute("role", "parameter"); + else // atom->string() == ATOM_FORMATTING_TELETYPE + m_inTeletype = true; + } else if (atom->string() == ATOM_FORMATTING_UICONTROL) { + m_writer->writeStartElement(dbNamespace, "guilabel"); + if (m_useITS) + m_writer->writeAttribute(itsNamespace, "translate", "no"); + } else if (atom->string() == ATOM_FORMATTING_TRADEMARK) { + m_writer->writeStartElement(dbNamespace, + appendTrademark(atom->find(Atom::FormattingRight)) ? + "trademark" : "phrase"); + if (m_useITS) + m_writer->writeAttribute(itsNamespace, "translate", "no"); + } else { + relative->location().warning(QStringLiteral("Unsupported formatting: %1").arg(atom->string())); + } + break; + case Atom::FormattingRight: + if (atom->string() == ATOM_FORMATTING_BOLD || atom->string() == ATOM_FORMATTING_ITALIC + || atom->string() == ATOM_FORMATTING_UNDERLINE + || atom->string() == ATOM_FORMATTING_SUBSCRIPT + || atom->string() == ATOM_FORMATTING_SUPERSCRIPT + || atom->string() == ATOM_FORMATTING_TELETYPE + || atom->string() == ATOM_FORMATTING_PARAMETER + || atom->string() == ATOM_FORMATTING_UICONTROL + || atom->string() == ATOM_FORMATTING_TRADEMARK) { + m_writer->writeEndElement(); + } else if (atom->string() == ATOM_FORMATTING_LINK) { + if (atom->string() == ATOM_FORMATTING_TELETYPE) + m_inTeletype = false; + endLink(); + } else { + relative->location().warning(QStringLiteral("Unsupported formatting: %1").arg(atom->string())); + } + break; + case Atom::AnnotatedList: { + if (const CollectionNode *cn = m_qdb->getCollectionNode(atom->string(), Node::Group)) + generateList(cn, atom->string(), Generator::sortOrder(atom->strings().last())); + } break; + case Atom::GeneratedList: { + const auto sortOrder{Generator::sortOrder(atom->strings().last())}; + bool hasGeneratedSomething = false; + if (atom->string() == QLatin1String("annotatedclasses") + || atom->string() == QLatin1String("attributions") + || atom->string() == QLatin1String("namespaces")) { + const NodeMultiMap things = atom->string() == QLatin1String("annotatedclasses") + ? m_qdb->getCppClasses() + : atom->string() == QLatin1String("attributions") ? m_qdb->getAttributions() + : m_qdb->getNamespaces(); + generateAnnotatedList(relative, things.values(), atom->string(), Auto, sortOrder); + hasGeneratedSomething = !things.isEmpty(); + } else if (atom->string() == QLatin1String("annotatedexamples") + || atom->string() == QLatin1String("annotatedattributions")) { + const NodeMultiMap things = atom->string() == QLatin1String("annotatedexamples") + ? m_qdb->getAttributions() + : m_qdb->getExamples(); + generateAnnotatedLists(relative, things, atom->string()); + hasGeneratedSomething = !things.isEmpty(); + } else if (atom->string() == QLatin1String("classes") + || atom->string() == QLatin1String("qmlbasictypes") // deprecated! + || atom->string() == QLatin1String("qmlvaluetypes") + || atom->string() == QLatin1String("qmltypes")) { + const NodeMultiMap things = atom->string() == QLatin1String("classes") + ? m_qdb->getCppClasses() + : (atom->string() == QLatin1String("qmlvaluetypes") + || atom->string() == QLatin1String("qmlbasictypes")) + ? m_qdb->getQmlValueTypes() + : m_qdb->getQmlTypes(); + generateCompactList(relative, things, true, QString(), atom->string()); + hasGeneratedSomething = !things.isEmpty(); + } else if (atom->string().contains("classes ")) { + QString rootName = atom->string().mid(atom->string().indexOf("classes") + 7).trimmed(); + NodeMultiMap things = m_qdb->getCppClasses(); + + hasGeneratedSomething = !things.isEmpty(); + generateCompactList(relative, things, true, rootName, atom->string()); + } else if ((idx = atom->string().indexOf(QStringLiteral("bymodule"))) != -1) { + QString moduleName = atom->string().mid(idx + 8).trimmed(); + Node::NodeType moduleType = typeFromString(atom); + QDocDatabase *qdb = QDocDatabase::qdocDB(); + if (const CollectionNode *cn = qdb->getCollectionNode(moduleName, moduleType)) { + NodeMap map; + switch (moduleType) { + case Node::Module: + // classesbymodule <module_name> + map = cn->getMembers([](const Node *n){ return n->isClassNode(); }); + break; + case Node::QmlModule: + if (atom->string().contains(QLatin1String("qmlvaluetypes"))) + map = cn->getMembers(Node::QmlValueType); // qmlvaluetypesbymodule <module_name> + else + map = cn->getMembers(Node::QmlType); // qmltypesbymodule <module_name> + break; + default: // fall back to generating all members + generateAnnotatedList(relative, cn->members(), atom->string(), Auto, sortOrder); + hasGeneratedSomething = !cn->members().isEmpty(); + break; + } + if (!map.isEmpty()) { + generateAnnotatedList(relative, map.values(), atom->string(), Auto, sortOrder); + hasGeneratedSomething = true; + } + } + } else if (atom->string() == QLatin1String("classhierarchy")) { + generateClassHierarchy(relative, m_qdb->getCppClasses()); + hasGeneratedSomething = !m_qdb->getCppClasses().isEmpty(); + } else if (atom->string().startsWith("obsolete")) { + QString prefix = atom->string().contains("cpp") ? QStringLiteral("Q") : QString(); + const NodeMultiMap &things = atom->string() == QLatin1String("obsoleteclasses") + ? m_qdb->getObsoleteClasses() + : atom->string() == QLatin1String("obsoleteqmltypes") + ? m_qdb->getObsoleteQmlTypes() + : atom->string() == QLatin1String("obsoletecppmembers") + ? m_qdb->getClassesWithObsoleteMembers() + : m_qdb->getQmlTypesWithObsoleteMembers(); + generateCompactList(relative, things, false, prefix, atom->string()); + hasGeneratedSomething = !things.isEmpty(); + } else if (atom->string() == QLatin1String("functionindex")) { + generateFunctionIndex(relative); + hasGeneratedSomething = !m_qdb->getFunctionIndex().isEmpty(); + } else if (atom->string() == QLatin1String("legalese")) { + generateLegaleseList(relative); + hasGeneratedSomething = !m_qdb->getLegaleseTexts().isEmpty(); + } else if (atom->string() == QLatin1String("overviews") + || atom->string() == QLatin1String("cpp-modules") + || atom->string() == QLatin1String("qml-modules") + || atom->string() == QLatin1String("related")) { + generateList(relative, atom->string()); + hasGeneratedSomething = true; // Approximation, because there is + // some nontrivial logic in generateList. + } else if (const auto *cn = m_qdb->getCollectionNode(atom->string(), Node::Group); cn) { + generateAnnotatedList(cn, cn->members(), atom->string(), ItemizedList, sortOrder); + hasGeneratedSomething = true; // Approximation + } + + // There must still be some content generated for the DocBook document + // to be valid (except if already in a paragraph). + if (!hasGeneratedSomething && !m_inPara) { + m_writer->writeEmptyElement(dbNamespace, "para"); + newLine(); + } + } + break; + case Atom::SinceList: + // Table of contents, should automatically be generated by the DocBook processor. + Q_FALLTHROUGH(); + case Atom::LineBreak: + case Atom::BR: + case Atom::HR: + // Not supported in DocBook. + break; + case Atom::Image: // mediaobject + // An Image atom is always followed by an ImageText atom, + // containing the alternative text. + // If no caption is present, we just output a <db:mediaobject>, + // avoiding the wrapper as it is not required. + // For bordered images, there is another atom before the + // caption, DivRight (the corresponding DivLeft being just + // before the image). + + if (atom->next() && matchAhead(atom->next(), Atom::DivRight) && atom->next()->next() + && matchAhead(atom->next()->next(), Atom::CaptionLeft)) { + // If there is a caption, there must be a <db:figure> + // wrapper starting with the caption. + Q_ASSERT(atom->next()); + Q_ASSERT(atom->next()->next()); + Q_ASSERT(atom->next()->next()->next()); + Q_ASSERT(atom->next()->next()->next()->next()); + Q_ASSERT(atom->next()->next()->next()->next()->next()); + + m_writer->writeStartElement(dbNamespace, "figure"); + newLine(); + + const Atom *current = atom->next()->next()->next(); + skipAhead += 2; + + Q_ASSERT(current->type() == Atom::CaptionLeft); + generateAtom(current, relative, nullptr); + current = current->next(); + ++skipAhead; + + while (current->type() != Atom::CaptionRight) { // The actual caption. + generateAtom(current, relative, nullptr); + current = current->next(); + ++skipAhead; + } + + Q_ASSERT(current->type() == Atom::CaptionRight); + generateAtom(current, relative, nullptr); + current = current->next(); + ++skipAhead; + + m_closeFigureWrapper = true; + } + + if (atom->next() && matchAhead(atom->next(), Atom::CaptionLeft)) { + // If there is a caption, there must be a <db:figure> + // wrapper starting with the caption. + Q_ASSERT(atom->next()); + Q_ASSERT(atom->next()->next()); + Q_ASSERT(atom->next()->next()->next()); + Q_ASSERT(atom->next()->next()->next()->next()); + + m_writer->writeStartElement(dbNamespace, "figure"); + newLine(); + + const Atom *current = atom->next()->next(); + ++skipAhead; + + Q_ASSERT(current->type() == Atom::CaptionLeft); + generateAtom(current, relative, nullptr); + current = current->next(); + ++skipAhead; + + while (current->type() != Atom::CaptionRight) { // The actual caption. + generateAtom(current, relative, nullptr); + current = current->next(); + ++skipAhead; + } + + Q_ASSERT(current->type() == Atom::CaptionRight); + generateAtom(current, relative, nullptr); + current = current->next(); + ++skipAhead; + + m_closeFigureWrapper = true; + } + + Q_FALLTHROUGH(); + case Atom::InlineImage: { // inlinemediaobject + // TODO: [generator-insufficient-structural-abstraction] + // The structure of the computations for this part of the + // docbook generation and the same parts in other format + // generators is the same. + // + // The difference, instead, lies in what the generated output + // is like. A correct abstraction for a generator would take + // this structural equivalence into account and encapsulate it + // into a driver for the format generators. + // + // This would avoid the replication of content, and the + // subsequent friction for changes and desynchronization + // between generators. + // + // Review all the generators routines and find the actual + // skeleton that is shared between them, then consider it when + // extracting the logic for the generation phase. + QString tag = atom->type() == Atom::Image ? "mediaobject" : "inlinemediaobject"; + m_writer->writeStartElement(dbNamespace, tag); + newLine(); + + auto maybe_resolved_file{file_resolver.resolve(atom->string())}; + if (!maybe_resolved_file) { + // TODO: [uncetnralized-admonition][failed-resolve-file] + relative->location().warning(QStringLiteral("Missing image: %1").arg(atom->string())); + + m_writer->writeStartElement(dbNamespace, "textobject"); + newLine(); + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeTextElement(dbNamespace, "emphasis", + "[Missing image " + atom->string() + "]"); + m_writer->writeEndElement(); // para + newLine(); + m_writer->writeEndElement(); // textobject + newLine(); + } else { + ResolvedFile file{*maybe_resolved_file}; + QString file_name{QFileInfo{file.get_path()}.fileName()}; + + // TODO: [uncentralized-output-directory-structure] + Config::copyFile(relative->doc().location(), file.get_path(), file_name, outputDir() + QLatin1String("/images")); + + if (atom->next() && !atom->next()->string().isEmpty() + && atom->next()->type() == Atom::ImageText) { + m_writer->writeTextElement(dbNamespace, "alt", atom->next()->string()); + newLine(); + } + + m_writer->writeStartElement(dbNamespace, "imageobject"); + newLine(); + m_writer->writeEmptyElement(dbNamespace, "imagedata"); + // TODO: [uncentralized-output-directory-structure] + m_writer->writeAttribute("fileref", "images/" + file_name); + newLine(); + m_writer->writeEndElement(); // imageobject + newLine(); + + // TODO: [uncentralized-output-directory-structure] + setImageFileName(relative, "images/" + file_name); + } + + m_writer->writeEndElement(); // [inline]mediaobject + if (atom->type() == Atom::Image) + newLine(); + + if (m_closeFigureWrapper) { + m_writer->writeEndElement(); // figure + newLine(); + m_closeFigureWrapper = false; + } + } break; + case Atom::ImageText: + break; + case Atom::ImportantLeft: + case Atom::NoteLeft: + case Atom::WarningLeft: { + QString admonType = atom->typeString().toLower(); + // Remove 'Left' to get the admonition type + admonType.chop(4); + m_writer->writeStartElement(dbNamespace, admonType); + newLine(); + m_writer->writeStartElement(dbNamespace, "para"); + m_inPara = true; + } break; + case Atom::ImportantRight: + case Atom::NoteRight: + case Atom::WarningRight: + m_writer->writeEndElement(); // para + m_inPara = false; + newLine(); + m_writer->writeEndElement(); // note/important + newLine(); + break; + case Atom::LegaleseLeft: + case Atom::LegaleseRight: + break; + case Atom::Link: + case Atom::NavLink: { + const Node *node = nullptr; + QString link = getLink(atom, relative, &node); + beginLink(link, node, relative); // Ended at Atom::FormattingRight + skipAhead = 1; + } break; + case Atom::LinkNode: { + const Node *node = CodeMarker::nodeForString(atom->string()); + beginLink(linkForNode(node, relative), node, relative); + skipAhead = 1; + } break; + case Atom::ListLeft: + if (m_inPara) { + // The variable m_inPara is not set in a very smart way, because + // it ignores nesting. This might in theory create false positives + // here. A better solution would be to track the depth of + // paragraphs the generator is in, but determining the right check + // for this condition is far from trivial (think of nested lists). + m_writer->writeEndElement(); // para + newLine(); + m_inPara = false; + } + + if (atom->string() == ATOM_LIST_BULLET) { + m_writer->writeStartElement(dbNamespace, "itemizedlist"); + newLine(); + } else if (atom->string() == ATOM_LIST_TAG) { + m_writer->writeStartElement(dbNamespace, "variablelist"); + newLine(); + } else if (atom->string() == ATOM_LIST_VALUE) { + m_writer->writeStartElement(dbNamespace, "informaltable"); + newLine(); + m_writer->writeStartElement(dbNamespace, "thead"); + newLine(); + m_writer->writeStartElement(dbNamespace, "tr"); + newLine(); + m_writer->writeTextElement(dbNamespace, "th", "Constant"); + newLine(); + + m_threeColumnEnumValueTable = isThreeColumnEnumValueTable(atom); + if (m_threeColumnEnumValueTable && relative->nodeType() == Node::Enum) { + // With three columns, if not in \enum topic, skip the value column + m_writer->writeTextElement(dbNamespace, "th", "Value"); + newLine(); + } + + if (!isOneColumnValueTable(atom)) { + m_writer->writeTextElement(dbNamespace, "th", "Description"); + newLine(); + } + + m_writer->writeEndElement(); // tr + newLine(); + m_writer->writeEndElement(); // thead + newLine(); + } else { // No recognized list type. + m_writer->writeStartElement(dbNamespace, "orderedlist"); + + if (atom->next() != nullptr && atom->next()->string().toInt() > 1) + m_writer->writeAttribute("startingnumber", atom->next()->string()); + + if (atom->string() == ATOM_LIST_UPPERALPHA) + m_writer->writeAttribute("numeration", "upperalpha"); + else if (atom->string() == ATOM_LIST_LOWERALPHA) + m_writer->writeAttribute("numeration", "loweralpha"); + else if (atom->string() == ATOM_LIST_UPPERROMAN) + m_writer->writeAttribute("numeration", "upperroman"); + else if (atom->string() == ATOM_LIST_LOWERROMAN) + m_writer->writeAttribute("numeration", "lowerroman"); + else // (atom->string() == ATOM_LIST_NUMERIC) + m_writer->writeAttribute("numeration", "arabic"); + + newLine(); + } + m_inList++; + break; + case Atom::ListItemNumber: + break; + case Atom::ListTagLeft: + if (atom->string() == ATOM_LIST_TAG) { + m_writer->writeStartElement(dbNamespace, "varlistentry"); + newLine(); + m_writer->writeStartElement(dbNamespace, "item"); + } else { // (atom->string() == ATOM_LIST_VALUE) + std::pair<QString, int> pair = getAtomListValue(atom); + skipAhead = pair.second; + + m_writer->writeStartElement(dbNamespace, "tr"); + newLine(); + m_writer->writeStartElement(dbNamespace, "td"); + newLine(); + m_writer->writeStartElement(dbNamespace, "para"); + if (m_useITS) + m_writer->writeAttribute(itsNamespace, "translate", "no"); + generateEnumValue(pair.first, relative); + m_writer->writeEndElement(); // para + newLine(); + m_writer->writeEndElement(); // td + newLine(); + + if (relative->nodeType() == Node::Enum) { + const auto enume = static_cast<const EnumNode *>(relative); + QString itemValue = enume->itemValue(atom->next()->string()); + + m_writer->writeStartElement(dbNamespace, "td"); + if (itemValue.isEmpty()) + m_writer->writeCharacters("?"); + else { + m_writer->writeStartElement(dbNamespace, "code"); + if (m_useITS) + m_writer->writeAttribute(itsNamespace, "translate", "no"); + m_writer->writeCharacters(itemValue); + m_writer->writeEndElement(); // code + } + m_writer->writeEndElement(); // td + newLine(); + } + } + m_inList++; + break; + case Atom::SinceTagRight: + if (atom->string() == ATOM_LIST_TAG) { + m_writer->writeEndElement(); // item + newLine(); + } + break; + case Atom::ListTagRight: + if (m_inList > 0 && atom->string() == ATOM_LIST_TAG) { + m_writer->writeEndElement(); // item + newLine(); + m_inList = false; + } + break; + case Atom::ListItemLeft: + if (m_inList > 0) { + m_inListItemLineOpen = false; + if (atom->string() == ATOM_LIST_TAG) { + m_writer->writeStartElement(dbNamespace, "listitem"); + newLine(); + m_writer->writeStartElement(dbNamespace, "para"); + m_inPara = true; + } else if (atom->string() == ATOM_LIST_VALUE) { + if (m_threeColumnEnumValueTable) { + if (matchAhead(atom, Atom::ListItemRight)) { + m_writer->writeEmptyElement(dbNamespace, "td"); + newLine(); + m_inListItemLineOpen = false; + } else { + m_writer->writeStartElement(dbNamespace, "td"); + newLine(); + m_inListItemLineOpen = true; + } + } + } else { + m_writer->writeStartElement(dbNamespace, "listitem"); + newLine(); + } + // Don't skip a paragraph, DocBook requires them within list items. + } + break; + case Atom::ListItemRight: + if (m_inList > 0) { + if (atom->string() == ATOM_LIST_TAG) { + m_writer->writeEndElement(); // para + m_inPara = false; + newLine(); + m_writer->writeEndElement(); // listitem + newLine(); + m_writer->writeEndElement(); // varlistentry + newLine(); + } else if (atom->string() == ATOM_LIST_VALUE) { + if (m_inListItemLineOpen) { + m_writer->writeEndElement(); // td + newLine(); + m_inListItemLineOpen = false; + } + m_writer->writeEndElement(); // tr + newLine(); + } else { + m_writer->writeEndElement(); // listitem + newLine(); + } + } + break; + case Atom::ListRight: + // Depending on atom->string(), closing a different item: + // - ATOM_LIST_BULLET: itemizedlist + // - ATOM_LIST_TAG: variablelist + // - ATOM_LIST_VALUE: informaltable + // - ATOM_LIST_NUMERIC: orderedlist + m_writer->writeEndElement(); + newLine(); + m_inList--; + break; + case Atom::Nop: + break; + case Atom::ParaLeft: + m_writer->writeStartElement(dbNamespace, "para"); + m_inPara = true; + break; + case Atom::ParaRight: + endLink(); + if (m_inPara) { + m_writer->writeEndElement(); // para + newLine(); + m_inPara = false; + } + break; + case Atom::QuotationLeft: + m_writer->writeStartElement(dbNamespace, "blockquote"); + m_inBlockquote = true; + break; + case Atom::QuotationRight: + m_writer->writeEndElement(); // blockquote + newLine(); + m_inBlockquote = false; + break; + case Atom::RawString: { + m_writer->device()->write(atom->string().toUtf8()); + } + break; + case Atom::SectionLeft: + m_hasSection = true; + + currentSectionLevel = atom->string().toInt() + hOffset(relative); + // Level 1 is dealt with at the header level (info tag). + if (currentSectionLevel > 1) { + // Unfortunately, SectionRight corresponds to the end of any section, + // i.e. going to a new section, even deeper. + while (!sectionLevels.empty() && sectionLevels.top() >= currentSectionLevel) { + sectionLevels.pop(); + m_writer->writeEndElement(); // section + newLine(); + } + + sectionLevels.push(currentSectionLevel); + + m_writer->writeStartElement(dbNamespace, "section"); + writeXmlId(Tree::refForAtom(atom)); + newLine(); + // Unlike startSectionBegin, don't start a title here. + } + + if (matchAhead(atom, Atom::SectionHeadingLeft) && + matchAhead(atom->next(), Atom::String) && + matchAhead(atom->next()->next(), Atom::SectionHeadingRight) && + matchAhead(atom->next()->next()->next(), Atom::SectionRight) && + !atom->next()->next()->next()->next()->next()) { + // A lonely section at the end of the document indicates that a + // generated list of some sort should be within this section. + // Close this section later on, in generateFooter(). + generateAtom(atom->next(), relative, nullptr); + generateAtom(atom->next()->next(), relative, nullptr); + generateAtom(atom->next()->next()->next(), relative, nullptr); + + m_closeSectionAfterGeneratedList = true; + skipAhead += 4; + sectionLevels.pop(); + } + + if (!matchAhead(atom, Atom::SectionHeadingLeft)) { + // No section title afterwards, make one up. This likely indicates a problem in the original documentation. + m_writer->writeTextElement(dbNamespace, "title", ""); + } + break; + case Atom::SectionRight: + // All the logic about closing sections is done in the SectionLeft case + // and generateFooter() for the end of the page. + break; + case Atom::SectionHeadingLeft: + // Level 1 is dealt with at the header level (info tag). + if (currentSectionLevel > 1) { + m_writer->writeStartElement(dbNamespace, "title"); + m_inSectionHeading = true; + } + break; + case Atom::SectionHeadingRight: + // Level 1 is dealt with at the header level (info tag). + if (currentSectionLevel > 1) { + m_writer->writeEndElement(); // title + newLine(); + m_inSectionHeading = false; + } + break; + case Atom::SidebarLeft: + m_writer->writeStartElement(dbNamespace, "sidebar"); + break; + case Atom::SidebarRight: + m_writer->writeEndElement(); // sidebar + newLine(); + break; + case Atom::String: + if (m_inLink && !m_inContents && !m_inSectionHeading) + generateLink(atom); + else + m_writer->writeCharacters(atom->string()); + break; + case Atom::TableLeft: { + std::pair<QString, QString> pair = getTableWidthAttr(atom); + QString attr = pair.second; + QString width = pair.first; + + if (m_inPara) { + m_writer->writeEndElement(); // para or blockquote + newLine(); + m_inPara = false; + } + + m_tableHeaderAlreadyOutput = false; + + m_writer->writeStartElement(dbNamespace, "informaltable"); + m_writer->writeAttribute("style", attr); + if (!width.isEmpty()) + m_writer->writeAttribute("width", width); + newLine(); + } break; + case Atom::TableRight: + m_tableWidthAttr = {"", ""}; + m_writer->writeEndElement(); // table + newLine(); + break; + case Atom::TableHeaderLeft: { + if (matchAhead(atom, Atom::TableHeaderRight)) { + ++skipAhead; + break; + } + + if (m_tableHeaderAlreadyOutput) { + // Headers are only allowed at the beginning of the table: close + // the table and reopen one. + m_writer->writeEndElement(); // table + newLine(); + + const QString &attr = m_tableWidthAttr.second; + const QString &width = m_tableWidthAttr.first; + + m_writer->writeStartElement(dbNamespace, "informaltable"); + m_writer->writeAttribute("style", attr); + if (!width.isEmpty()) + m_writer->writeAttribute("width", width); + newLine(); + } else { + m_tableHeaderAlreadyOutput = true; + } + + const Atom *next = atom->next(); + QString id{""}; + if (matchAhead(atom, Atom::Target)) { + id = Utilities::asAsciiPrintable(next->string()); + next = next->next(); + ++skipAhead; + } + + m_writer->writeStartElement(dbNamespace, "thead"); + newLine(); + m_writer->writeStartElement(dbNamespace, "tr"); + writeXmlId(id); + newLine(); + m_inTableHeader = true; + + if (!matchAhead(atom, Atom::TableItemLeft)) { + m_closeTableCell = true; + m_writer->writeStartElement(dbNamespace, "td"); + newLine(); + } + } + break; + case Atom::TableHeaderRight: + if (m_closeTableCell) { + m_closeTableCell = false; + m_writer->writeEndElement(); // td + newLine(); + } + + m_writer->writeEndElement(); // tr + newLine(); + if (matchAhead(atom, Atom::TableHeaderLeft)) { + skipAhead = 1; + m_writer->writeStartElement(dbNamespace, "tr"); + newLine(); + } else { + m_writer->writeEndElement(); // thead + newLine(); + m_inTableHeader = false; + } + break; + case Atom::TableRowLeft: { + if (matchAhead(atom, Atom::TableRowRight)) { + skipAhead = 1; + break; + } + + QString id{""}; + bool hasTarget {false}; + if (matchAhead(atom, Atom::Target)) { + id = Utilities::asAsciiPrintable(atom->next()->string()); + ++skipAhead; + hasTarget = true; + } + + m_writer->writeStartElement(dbNamespace, "tr"); + writeXmlId(id); + + if (atom->string().isEmpty()) { + m_writer->writeAttribute("valign", "top"); + } else { + // Basic parsing of attributes, should be enough. The input string (atom->string()) + // looks like: + // arg1="val1" arg2="val2" + QStringList args = atom->string().split("\"", Qt::SkipEmptyParts); + // arg1=, val1, arg2=, val2, + // \-- 1st --/ \-- 2nd --/ \-- remainder + const int nArgs = args.size(); + + if (nArgs % 2) { + // Problem... + relative->doc().location().warning( + QStringLiteral("Error when parsing attributes for the table: got \"%1\"") + .arg(atom->string())); + } + for (int i = 0; i + 1 < nArgs; i += 2) { + // args.at(i): name of the attribute being set. + // args.at(i + 1): value of the said attribute. + const QString &attr = args.at(i).chopped(1); + if (attr == "id") { // Too bad if there is an anchor later on + // (currently never happens). + writeXmlId(args.at(i + 1)); + } else { + m_writer->writeAttribute(attr, args.at(i + 1)); + } + } + } + newLine(); + + // If there is nothing in this row, close it right now. There might be keywords before the row contents. + bool isRowEmpty = hasTarget ? !matchAhead(atom->next(), Atom::TableItemLeft) : !matchAhead(atom, Atom::TableItemLeft); + if (isRowEmpty && matchAhead(atom, Atom::Keyword)) { + const Atom* next = atom->next(); + while (matchAhead(next, Atom::Keyword)) + next = next->next(); + isRowEmpty = !matchAhead(next, Atom::TableItemLeft); + } + + if (isRowEmpty) { + m_closeTableRow = true; + m_writer->writeEndElement(); // td + newLine(); + } + } + break; + case Atom::TableRowRight: + if (m_closeTableRow) { + m_closeTableRow = false; + m_writer->writeEndElement(); // td + newLine(); + } + + m_writer->writeEndElement(); // tr + newLine(); + break; + case Atom::TableItemLeft: + m_writer->writeStartElement(dbNamespace, m_inTableHeader ? "th" : "td"); + + for (int i = 0; i < atom->count(); ++i) { + const QString &p = atom->string(i); + if (p.contains('=')) { + QStringList lp = p.split(QLatin1Char('=')); + m_writer->writeAttribute(lp.at(0), lp.at(1)); + } else { + QStringList spans = p.split(QLatin1Char(',')); + if (spans.size() == 2) { + if (spans.at(0) != "1") + m_writer->writeAttribute("colspan", spans.at(0).trimmed()); + if (spans.at(1) != "1") + m_writer->writeAttribute("rowspan", spans.at(1).trimmed()); + } + } + } + newLine(); + // No skipahead, as opposed to HTML: in DocBook, the text must be wrapped in paragraphs. + break; + case Atom::TableItemRight: + m_writer->writeEndElement(); // th if m_inTableHeader, otherwise td + newLine(); + break; + case Atom::TableOfContents: + Q_FALLTHROUGH(); + case Atom::Keyword: + break; + case Atom::Target: + // Sometimes, there is a \target just before a section title with the same ID. Only output one xml:id. + if (matchAhead(atom, Atom::SectionRight) && matchAhead(atom->next(), Atom::SectionLeft)) { + QString nextId = Utilities::asAsciiPrintable( + Text::sectionHeading(atom->next()->next()).toString()); + QString ownId = Utilities::asAsciiPrintable(atom->string()); + if (nextId == ownId) + break; + } + + writeAnchor(Utilities::asAsciiPrintable(atom->string())); + break; + case Atom::UnhandledFormat: + m_writer->writeStartElement(dbNamespace, "emphasis"); + m_writer->writeAttribute("role", "bold"); + m_writer->writeCharacters("<Missing DocBook>"); + m_writer->writeEndElement(); // emphasis + break; + case Atom::UnknownCommand: + m_writer->writeStartElement(dbNamespace, "emphasis"); + m_writer->writeAttribute("role", "bold"); + if (m_useITS) + m_writer->writeAttribute(itsNamespace, "translate", "no"); + m_writer->writeCharacters("<Unknown command>"); + m_writer->writeStartElement(dbNamespace, "code"); + m_writer->writeCharacters(atom->string()); + m_writer->writeEndElement(); // code + m_writer->writeEndElement(); // emphasis + break; + case Atom::CodeQuoteArgument: + case Atom::CodeQuoteCommand: + case Atom::ComparesLeft: + case Atom::ComparesRight: + case Atom::SnippetCommand: + case Atom::SnippetIdentifier: + case Atom::SnippetLocation: + // No output (ignore). + break; + default: + unknownAtom(atom); + } + return skipAhead; +} + +void DocBookGenerator::generateClassHierarchy(const Node *relative, NodeMultiMap &classMap) +{ + // From HtmlGenerator::generateClassHierarchy. + if (classMap.isEmpty()) + return; + + std::function<void(ClassNode *)> generateClassAndChildren + = [this, &relative, &generateClassAndChildren](ClassNode * classe) { + m_writer->writeStartElement(dbNamespace, "listitem"); + newLine(); + + // This class. + m_writer->writeStartElement(dbNamespace, "para"); + generateFullName(classe, relative); + m_writer->writeEndElement(); // para + newLine(); + + // Children, if any. + bool hasChild = false; + for (const RelatedClass &relatedClass : classe->derivedClasses()) { + if (relatedClass.m_node && relatedClass.m_node->isInAPI()) { + hasChild = true; + break; + } + } + + if (hasChild) { + m_writer->writeStartElement(dbNamespace, "itemizedlist"); + newLine(); + + for (const RelatedClass &relatedClass: classe->derivedClasses()) { + if (relatedClass.m_node && relatedClass.m_node->isInAPI()) { + generateClassAndChildren(relatedClass.m_node); + } + } + + m_writer->writeEndElement(); // itemizedlist + newLine(); + } + + // End this class. + m_writer->writeEndElement(); // listitem + newLine(); + }; + + m_writer->writeStartElement(dbNamespace, "itemizedlist"); + newLine(); + + for (const auto &it : classMap) { + auto *classe = static_cast<ClassNode *>(it); + if (classe->baseClasses().isEmpty()) + generateClassAndChildren(classe); + } + + m_writer->writeEndElement(); // itemizedlist + newLine(); +} + +void DocBookGenerator::generateLink(const Atom *atom) +{ + Q_ASSERT(m_inLink); + + // From HtmlGenerator::generateLink. + if (m_linkNode && m_linkNode->isFunction()) { + auto match = XmlGenerator::m_funcLeftParen.match(atom->string()); + if (match.hasMatch()) { + // C++: move () outside of link + qsizetype leftParenLoc = match.capturedStart(1); + m_writer->writeCharacters(atom->string().left(leftParenLoc)); + endLink(); + m_writer->writeCharacters(atom->string().mid(leftParenLoc)); + return; + } + } + m_writer->writeCharacters(atom->string()); +} + +/*! + This version of the function is called when the \a link is known + to be correct. + */ +void DocBookGenerator::beginLink(const QString &link, const Node *node, const Node *relative) +{ + // From HtmlGenerator::beginLink. + m_writer->writeStartElement(dbNamespace, "link"); + m_writer->writeAttribute(xlinkNamespace, "href", link); + if (node && !(relative && node->status() == relative->status()) + && node->isDeprecated()) + m_writer->writeAttribute("role", "deprecated"); + m_inLink = true; + m_linkNode = node; +} + +void DocBookGenerator::endLink() +{ + // From HtmlGenerator::endLink. + if (m_inLink) + m_writer->writeEndElement(); // link + m_inLink = false; + m_linkNode = nullptr; +} + +void DocBookGenerator::generateList(const Node *relative, const QString &selector, + Qt::SortOrder sortOrder) +{ + // From HtmlGenerator::generateList, without warnings, changing prototype. + CNMap cnm; + Node::NodeType type = Node::NoType; + if (selector == QLatin1String("overviews")) + type = Node::Group; + else if (selector == QLatin1String("cpp-modules")) + type = Node::Module; + else if (selector == QLatin1String("qml-modules")) + type = Node::QmlModule; + + if (type != Node::NoType) { + NodeList nodeList; + m_qdb->mergeCollections(type, cnm, relative); + const QList<CollectionNode *> collectionList = cnm.values(); + nodeList.reserve(collectionList.size()); + for (auto *collectionNode : collectionList) + nodeList.append(collectionNode); + generateAnnotatedList(relative, nodeList, selector, Auto, sortOrder); + } else { + /* + \generatelist {selector} is only allowed in a comment where + the topic is \group, \module, or \qmlmodule. + */ + Node *n = const_cast<Node *>(relative); + auto *cn = static_cast<CollectionNode *>(n); + m_qdb->mergeCollections(cn); + generateAnnotatedList(cn, cn->members(), selector, Auto, sortOrder); + } +} + +/*! + Outputs an annotated list of the nodes in \a nodeList. + A two-column table is output. + */ +void DocBookGenerator::generateAnnotatedList(const Node *relative, const NodeList &nodeList, + const QString &selector, GeneratedListType type, + Qt::SortOrder sortOrder) +{ + if (nodeList.isEmpty()) + return; + + // Do nothing if all items are internal or obsolete. + if (std::all_of(nodeList.cbegin(), nodeList.cend(), [](const Node *n) { + return n->isInternal() || n->isDeprecated(); })) { + return; + } + + // Detect if there is a need for a variablelist (i.e. titles mapped to + // descriptions) or a regular itemizedlist (only titles). + bool noItemsHaveTitle = + type == ItemizedList || std::all_of(nodeList.begin(), nodeList.end(), + [](const Node* node) { + return node->doc().briefText().toString().isEmpty(); + }); + + // Wrap the list in a section if needed. + if (type == AutoSection && m_hasSection) + startSection("", "Contents"); + + // From WebXMLGenerator::generateAnnotatedList. + if (!nodeList.isEmpty()) { + m_writer->writeStartElement(dbNamespace, noItemsHaveTitle ? "itemizedlist" : "variablelist"); + m_writer->writeAttribute("role", selector); + newLine(); + + NodeList members{nodeList}; + if (sortOrder == Qt::DescendingOrder) + std::sort(members.rbegin(), members.rend(), Node::nodeSortKeyOrNameLessThan); + else + std::sort(members.begin(), members.end(), Node::nodeSortKeyOrNameLessThan); + for (const auto &node : std::as_const(members)) { + if (node->isInternal() || node->isDeprecated()) + continue; + + if (noItemsHaveTitle) { + m_writer->writeStartElement(dbNamespace, "listitem"); + newLine(); + m_writer->writeStartElement(dbNamespace, "para"); + } else { + m_writer->writeStartElement(dbNamespace, "varlistentry"); + newLine(); + m_writer->writeStartElement(dbNamespace, "term"); + } + generateFullName(node, relative); + if (noItemsHaveTitle) { + m_writer->writeEndElement(); // para + newLine(); + m_writer->writeEndElement(); // listitem + } else { + m_writer->writeEndElement(); // term + newLine(); + m_writer->writeStartElement(dbNamespace, "listitem"); + newLine(); + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeCharacters(node->doc().briefText().toString()); + m_writer->writeEndElement(); // para + newLine(); + m_writer->writeEndElement(); // listitem + newLine(); + m_writer->writeEndElement(); // varlistentry + } + newLine(); + } + + m_writer->writeEndElement(); // itemizedlist or variablelist + newLine(); + } + + if (type == AutoSection && m_hasSection) + endSection(); +} + +/*! + Outputs a series of annotated lists from the nodes in \a nmm, + divided into sections based by the key names in the multimap. + */ +void DocBookGenerator::generateAnnotatedLists(const Node *relative, const NodeMultiMap &nmm, + const QString &selector) +{ + // From HtmlGenerator::generateAnnotatedLists. + for (const QString &name : nmm.uniqueKeys()) { + if (!name.isEmpty()) + startSection(name.toLower(), name); + generateAnnotatedList(relative, nmm.values(name), selector); + if (!name.isEmpty()) + endSection(); + } +} + +/*! + This function finds the common prefix of the names of all + the classes in the class map \a nmm and then generates a + compact list of the class names alphabetized on the part + of the name not including the common prefix. You can tell + the function to use \a comonPrefix as the common prefix, + but normally you let it figure it out itself by looking at + the name of the first and last classes in the class map + \a nmm. + */ +void DocBookGenerator::generateCompactList(const Node *relative, const NodeMultiMap &nmm, + bool includeAlphabet, const QString &commonPrefix, + const QString &selector) +{ + // From HtmlGenerator::generateCompactList. No more "includeAlphabet", this should be handled by + // the DocBook toolchain afterwards. + // TODO: In DocBook, probably no need for this method: this is purely presentational, i.e. to be + // fully handled by the DocBook toolchain. + + if (nmm.isEmpty()) + return; + + const int NumParagraphs = 37; // '0' to '9', 'A' to 'Z', '_' + qsizetype commonPrefixLen = commonPrefix.size(); + + /* + Divide the data into 37 paragraphs: 0, ..., 9, A, ..., Z, + underscore (_). QAccel will fall in paragraph 10 (A) and + QXtWidget in paragraph 33 (X). This is the only place where we + assume that NumParagraphs is 37. Each paragraph is a NodeMultiMap. + */ + NodeMultiMap paragraph[NumParagraphs + 1]; + QString paragraphName[NumParagraphs + 1]; + QSet<char> usedParagraphNames; + + for (auto c = nmm.constBegin(); c != nmm.constEnd(); ++c) { + QStringList pieces = c.key().split("::"); + int idx = commonPrefixLen; + if (idx > 0 && !pieces.last().startsWith(commonPrefix, Qt::CaseInsensitive)) + idx = 0; + QString last = pieces.last().toLower(); + QString key = last.mid(idx); + + int paragraphNr = NumParagraphs - 1; + + if (key[0].digitValue() != -1) { + paragraphNr = key[0].digitValue(); + } else if (key[0] >= QLatin1Char('a') && key[0] <= QLatin1Char('z')) { + paragraphNr = 10 + key[0].unicode() - 'a'; + } + + paragraphName[paragraphNr] = key[0].toUpper(); + usedParagraphNames.insert(key[0].toLower().cell()); + paragraph[paragraphNr].insert(last, c.value()); + } + + /* + Each paragraph j has a size: paragraph[j].count(). In the + discussion, we will assume paragraphs 0 to 5 will have sizes + 3, 1, 4, 1, 5, 9. + + We now want to compute the paragraph offset. Paragraphs 0 to 6 + start at offsets 0, 3, 4, 8, 9, 14, 23. + */ + int paragraphOffset[NumParagraphs + 1]; // 37 + 1 + paragraphOffset[0] = 0; + for (int i = 0; i < NumParagraphs; i++) // i = 0..36 + paragraphOffset[i + 1] = paragraphOffset[i] + paragraph[i].size(); + + // Output the alphabet as a row of links. + if (includeAlphabet && !usedParagraphNames.isEmpty()) { + m_writer->writeStartElement(dbNamespace, "simplelist"); + newLine(); + + for (int i = 0; i < 26; i++) { + QChar ch('a' + i); + if (usedParagraphNames.contains(char('a' + i))) { + m_writer->writeStartElement(dbNamespace, "member"); + generateSimpleLink(ch, ch.toUpper()); + m_writer->writeEndElement(); // member + newLine(); + } + } + + m_writer->writeEndElement(); // simplelist + newLine(); + } + + // Actual output. + int curParNr = 0; + int curParOffset = 0; + QString previousName; + bool multipleOccurrences = false; + + m_writer->writeStartElement(dbNamespace, "variablelist"); + m_writer->writeAttribute("role", selector); + newLine(); + + for (int i = 0; i < nmm.size(); i++) { + while ((curParNr < NumParagraphs) && (curParOffset == paragraph[curParNr].size())) { + + ++curParNr; + curParOffset = 0; + } + + // Starting a new paragraph means starting a new varlistentry. + if (curParOffset == 0) { + if (i > 0) { + m_writer->writeEndElement(); // itemizedlist + newLine(); + m_writer->writeEndElement(); // listitem + newLine(); + m_writer->writeEndElement(); // varlistentry + newLine(); + } + + m_writer->writeStartElement(dbNamespace, "varlistentry"); + if (includeAlphabet) + writeXmlId(paragraphName[curParNr][0].toLower()); + newLine(); + + m_writer->writeStartElement(dbNamespace, "term"); + m_writer->writeStartElement(dbNamespace, "emphasis"); + m_writer->writeAttribute("role", "bold"); + m_writer->writeCharacters(paragraphName[curParNr]); + m_writer->writeEndElement(); // emphasis + m_writer->writeEndElement(); // term + newLine(); + + m_writer->writeStartElement(dbNamespace, "listitem"); + newLine(); + m_writer->writeStartElement(dbNamespace, "itemizedlist"); + newLine(); + } + + // Output a listitem for the current offset in the current paragraph. + m_writer->writeStartElement(dbNamespace, "listitem"); + newLine(); + m_writer->writeStartElement(dbNamespace, "para"); + + if ((curParNr < NumParagraphs) && !paragraphName[curParNr].isEmpty()) { + NodeMultiMap::Iterator it; + NodeMultiMap::Iterator next; + it = paragraph[curParNr].begin(); + for (int j = 0; j < curParOffset; j++) + ++it; + + // Cut the name into pieces to determine whether it is simple (one piece) or complex + // (more than one piece). + QStringList pieces{it.value()->fullName(relative).split("::"_L1)}; + const auto &name{pieces.last()}; + next = it; + ++next; + if (name != previousName) + multipleOccurrences = false; + if ((next != paragraph[curParNr].end()) && (name == next.value()->name())) { + multipleOccurrences = true; + previousName = name; + } + if (multipleOccurrences && pieces.size() == 1) + pieces.last().append(": "_L1.arg(it.value()->tree()->camelCaseModuleName())); + + // Write the link to the element, which is identical if the element is obsolete or not. + m_writer->writeStartElement(dbNamespace, "link"); + m_writer->writeAttribute(xlinkNamespace, "href", linkForNode(*it, relative)); + if (const QString type = targetType(it.value()); !type.isEmpty()) + m_writer->writeAttribute("role", type); + m_writer->writeCharacters(pieces.last()); + m_writer->writeEndElement(); // link + + // Outside the link, give the full name of the node if it is complex. + if (pieces.size() > 1) { + m_writer->writeCharacters(" ("); + generateFullName(it.value()->parent(), relative); + m_writer->writeCharacters(")"); + } + } + + m_writer->writeEndElement(); // para + newLine(); + m_writer->writeEndElement(); // listitem + newLine(); + + curParOffset++; + } + m_writer->writeEndElement(); // itemizedlist + newLine(); + m_writer->writeEndElement(); // listitem + newLine(); + m_writer->writeEndElement(); // varlistentry + newLine(); + + m_writer->writeEndElement(); // variablelist + newLine(); +} + +void DocBookGenerator::generateFunctionIndex(const Node *relative) +{ + // From HtmlGenerator::generateFunctionIndex. + + // First list: links to parts of the second list, one item per letter. + m_writer->writeStartElement(dbNamespace, "simplelist"); + m_writer->writeAttribute("role", "functionIndex"); + newLine(); + for (int i = 0; i < 26; i++) { + QChar ch('a' + i); + m_writer->writeStartElement(dbNamespace, "member"); + m_writer->writeAttribute(xlinkNamespace, "href", QString("#") + ch); + m_writer->writeCharacters(ch.toUpper()); + m_writer->writeEndElement(); // member + newLine(); + } + m_writer->writeEndElement(); // simplelist + newLine(); + + // Second list: the actual list of functions, sorted by alphabetical + // order. One entry of the list per letter. + if (m_qdb->getFunctionIndex().isEmpty()) + return; + char nextLetter = 'a'; + char currentLetter; + + m_writer->writeStartElement(dbNamespace, "itemizedlist"); + newLine(); + + NodeMapMap &funcIndex = m_qdb->getFunctionIndex(); + QMap<QString, NodeMap>::ConstIterator f = funcIndex.constBegin(); + while (f != funcIndex.constEnd()) { + m_writer->writeStartElement(dbNamespace, "listitem"); + newLine(); + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeCharacters(f.key() + ": "); + + currentLetter = f.key()[0].unicode(); + while (islower(currentLetter) && currentLetter >= nextLetter) { + writeAnchor(QString(nextLetter)); + nextLetter++; + } + + NodeMap::ConstIterator s = (*f).constBegin(); + while (s != (*f).constEnd()) { + m_writer->writeCharacters(" "); + generateFullName((*s)->parent(), relative); + ++s; + } + + m_writer->writeEndElement(); // para + newLine(); + m_writer->writeEndElement(); // listitem + newLine(); + ++f; + } + m_writer->writeEndElement(); // itemizedlist + newLine(); +} + +void DocBookGenerator::generateLegaleseList(const Node *relative) +{ + // From HtmlGenerator::generateLegaleseList. + TextToNodeMap &legaleseTexts = m_qdb->getLegaleseTexts(); + for (auto it = legaleseTexts.cbegin(), end = legaleseTexts.cend(); it != end; ++it) { + Text text = it.key(); + generateText(text, relative); + m_writer->writeStartElement(dbNamespace, "itemizedlist"); + newLine(); + do { + m_writer->writeStartElement(dbNamespace, "listitem"); + newLine(); + m_writer->writeStartElement(dbNamespace, "para"); + generateFullName(it.value(), relative); + m_writer->writeEndElement(); // para + newLine(); + m_writer->writeEndElement(); // listitem + newLine(); + ++it; + } while (it != legaleseTexts.constEnd() && it.key() == text); + m_writer->writeEndElement(); // itemizedlist + newLine(); + } +} + +void DocBookGenerator::generateBrief(const Node *node) +{ + // From HtmlGenerator::generateBrief. Also see generateHeader, which is specifically dealing + // with the DocBook header (and thus wraps the brief in an abstract). + Text brief = node->doc().briefText(); + + if (!brief.isEmpty()) { + if (!brief.lastAtom()->string().endsWith('.')) + brief << Atom(Atom::String, "."); + + m_writer->writeStartElement(dbNamespace, "para"); + generateText(brief, node); + m_writer->writeEndElement(); // para + newLine(); + } +} + +bool DocBookGenerator::generateSince(const Node *node) +{ + // From Generator::generateSince. + if (!node->since().isEmpty()) { + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeCharacters("This " + typeString(node) + " was introduced in "); + m_writer->writeCharacters(formatSince(node) + "."); + m_writer->writeEndElement(); // para + newLine(); + + return true; + } + + return false; +} + +/*! + Generate the DocBook header for the file, including the abstract. + Equivalent to calling generateTitle and generateBrief in HTML. +*/ +void DocBookGenerator::generateHeader(const QString &title, const QString &subTitle, + const Node *node) +{ + refMap.clear(); + + // Output the DocBook header. + m_writer->writeStartElement(dbNamespace, "info"); + newLine(); + m_writer->writeStartElement(dbNamespace, "title"); + if (node->genus() & Node::API && m_useITS) + m_writer->writeAttribute(itsNamespace, "translate", "no"); + m_writer->writeCharacters(title); + m_writer->writeEndElement(); // title + newLine(); + + if (!subTitle.isEmpty()) { + m_writer->writeStartElement(dbNamespace, "subtitle"); + if (node->genus() & Node::API && m_useITS) + m_writer->writeAttribute(itsNamespace, "translate", "no"); + m_writer->writeCharacters(subTitle); + m_writer->writeEndElement(); // subtitle + newLine(); + } + + if (!m_project.isEmpty()) { + m_writer->writeTextElement(dbNamespace, "productname", m_project); + newLine(); + } + + if (!m_buildVersion.isEmpty()) { + m_writer->writeTextElement(dbNamespace, "edition", m_buildVersion); + newLine(); + } + + if (!m_projectDescription.isEmpty()) { + m_writer->writeTextElement(dbNamespace, "titleabbrev", m_projectDescription); + newLine(); + } + + // Deal with links. + // Adapted from HtmlGenerator::generateHeader (output part: no need to update a navigationLinks + // or useSeparator field, as this content is only output in the info tag, not in the main + // content). + if (node && !node->links().empty()) { + std::pair<QString, QString> linkPair; + std::pair<QString, QString> anchorPair; + const Node *linkNode; + + if (node->links().contains(Node::PreviousLink)) { + linkPair = node->links()[Node::PreviousLink]; + linkNode = m_qdb->findNodeForTarget(linkPair.first, node); + if (!linkNode || linkNode == node) + anchorPair = linkPair; + else + anchorPair = anchorForNode(linkNode); + + m_writer->writeStartElement(dbNamespace, "extendedlink"); + m_writer->writeAttribute(xlinkNamespace, "type", "extended"); + m_writer->writeEmptyElement(dbNamespace, "link"); + m_writer->writeAttribute(xlinkNamespace, "to", anchorPair.first); + m_writer->writeAttribute(xlinkNamespace, "type", "arc"); + m_writer->writeAttribute(xlinkNamespace, "arcrole", "prev"); + if (linkPair.first == linkPair.second && !anchorPair.second.isEmpty()) + m_writer->writeAttribute(xlinkNamespace, "title", anchorPair.second); + else + m_writer->writeAttribute(xlinkNamespace, "title", linkPair.second); + m_writer->writeEndElement(); // extendedlink + newLine(); + } + if (node->links().contains(Node::NextLink)) { + linkPair = node->links()[Node::NextLink]; + linkNode = m_qdb->findNodeForTarget(linkPair.first, node); + if (!linkNode || linkNode == node) + anchorPair = linkPair; + else + anchorPair = anchorForNode(linkNode); + + m_writer->writeStartElement(dbNamespace, "extendedlink"); + m_writer->writeAttribute(xlinkNamespace, "type", "extended"); + m_writer->writeEmptyElement(dbNamespace, "link"); + m_writer->writeAttribute(xlinkNamespace, "to", anchorPair.first); + m_writer->writeAttribute(xlinkNamespace, "type", "arc"); + m_writer->writeAttribute(xlinkNamespace, "arcrole", "next"); + if (linkPair.first == linkPair.second && !anchorPair.second.isEmpty()) + m_writer->writeAttribute(xlinkNamespace, "title", anchorPair.second); + else + m_writer->writeAttribute(xlinkNamespace, "title", linkPair.second); + m_writer->writeEndElement(); // extendedlink + newLine(); + } + if (node->links().contains(Node::StartLink)) { + linkPair = node->links()[Node::StartLink]; + linkNode = m_qdb->findNodeForTarget(linkPair.first, node); + if (!linkNode || linkNode == node) + anchorPair = linkPair; + else + anchorPair = anchorForNode(linkNode); + + m_writer->writeStartElement(dbNamespace, "extendedlink"); + m_writer->writeAttribute(xlinkNamespace, "type", "extended"); + m_writer->writeEmptyElement(dbNamespace, "link"); + m_writer->writeAttribute(xlinkNamespace, "to", anchorPair.first); + m_writer->writeAttribute(xlinkNamespace, "type", "arc"); + m_writer->writeAttribute(xlinkNamespace, "arcrole", "start"); + if (linkPair.first == linkPair.second && !anchorPair.second.isEmpty()) + m_writer->writeAttribute(xlinkNamespace, "title", anchorPair.second); + else + m_writer->writeAttribute(xlinkNamespace, "title", linkPair.second); + m_writer->writeEndElement(); // extendedlink + newLine(); + } + } + + // Deal with the abstract (what qdoc calls brief). + if (node) { + // Adapted from HtmlGenerator::generateBrief, without extraction marks. The parameter + // addLink is always false. Factoring this function out is not as easy as in HtmlGenerator: + // abstracts only happen in the header (info tag), slightly different tags must be used at + // other places. Also includes code from HtmlGenerator::generateCppReferencePage to handle + // the name spaces. + m_writer->writeStartElement(dbNamespace, "abstract"); + newLine(); + + bool generatedSomething = false; + + Text brief; + const NamespaceNode *ns = + node->isNamespace() ? static_cast<const NamespaceNode *>(node) : nullptr; + if (ns && !ns->hasDoc() && ns->docNode()) { + NamespaceNode *NS = ns->docNode(); + brief << "The " << ns->name() + << " namespace includes the following elements from module " + << ns->tree()->camelCaseModuleName() << ". The full namespace is " + << "documented in module " << NS->tree()->camelCaseModuleName() + << Atom(Atom::LinkNode, fullDocumentLocation(NS)) + << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) + << Atom(Atom::String, " here.") + << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); + } else { + brief = node->doc().briefText(); + } + + if (!brief.isEmpty()) { + if (!brief.lastAtom()->string().endsWith('.')) + brief << Atom(Atom::String, "."); + + m_writer->writeStartElement(dbNamespace, "para"); + generateText(brief, node); + m_writer->writeEndElement(); // para + newLine(); + + generatedSomething = true; + } + + // Generate other paragraphs that should go into the abstract. + generatedSomething |= generateStatus(node); + generatedSomething |= generateSince(node); + generatedSomething |= generateThreadSafeness(node); + generatedSomething |= generateComparisonCategory(node); + generatedSomething |= generateComparisonList(node); + + // An abstract cannot be empty, hence use the project description. + if (!generatedSomething) + m_writer->writeTextElement(dbNamespace, "para", m_projectDescription + "."); + + m_writer->writeEndElement(); // abstract + newLine(); + } + + // End of the DocBook header. + m_writer->writeEndElement(); // info + newLine(); +} + +void DocBookGenerator::closeTextSections() +{ + while (!sectionLevels.isEmpty()) { + sectionLevels.pop(); + endSection(); + } +} + +void DocBookGenerator::generateFooter() +{ + if (m_closeSectionAfterGeneratedList) { + m_closeSectionAfterGeneratedList = false; + endSection(); + } + if (m_closeSectionAfterRawTitle) { + m_closeSectionAfterRawTitle = false; + endSection(); + } + + closeTextSections(); + m_writer->writeEndElement(); // article +} + +void DocBookGenerator::generateSimpleLink(const QString &href, const QString &text) +{ + m_writer->writeStartElement(dbNamespace, "link"); + m_writer->writeAttribute(xlinkNamespace, "href", href); + m_writer->writeCharacters(text); + m_writer->writeEndElement(); // link +} + +void DocBookGenerator::generateObsoleteMembers(const Sections §ions) +{ + // From HtmlGenerator::generateObsoleteMembersFile. + SectionPtrVector summary_spv; // Summaries are ignored in DocBook (table of contents). + SectionPtrVector details_spv; + if (!sections.hasObsoleteMembers(&summary_spv, &details_spv)) + return; + + Aggregate *aggregate = sections.aggregate(); + startSection("obsolete", "Obsolete Members for " + aggregate->name()); + + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeStartElement(dbNamespace, "emphasis"); + m_writer->writeAttribute("role", "bold"); + m_writer->writeCharacters("The following members of class "); + generateSimpleLink(linkForNode(aggregate, nullptr), aggregate->name()); + m_writer->writeCharacters(" are deprecated."); + m_writer->writeEndElement(); // emphasis bold + m_writer->writeCharacters(" We strongly advise against using them in new code."); + m_writer->writeEndElement(); // para + newLine(); + + for (const Section *section : details_spv) { + const QString &title = "Obsolete " + section->title(); + startSection(title.toLower(), title); + + const NodeVector &members = section->obsoleteMembers(); + NodeVector::ConstIterator m = members.constBegin(); + while (m != members.constEnd()) { + if ((*m)->access() != Access::Private) + generateDetailedMember(*m, aggregate); + ++m; + } + + endSection(); + } + + endSection(); +} + +/*! + Generates a separate section where obsolete members of the QML + type \a qcn are listed. The \a marker is used to generate + the section lists, which are then traversed and output here. + + Note that this function currently only handles correctly the + case where \a status is \c {Section::Deprecated}. + */ +void DocBookGenerator::generateObsoleteQmlMembers(const Sections §ions) +{ + // From HtmlGenerator::generateObsoleteQmlMembersFile. + SectionPtrVector summary_spv; // Summaries are not useful in DocBook. + SectionPtrVector details_spv; + if (!sections.hasObsoleteMembers(&summary_spv, &details_spv)) + return; + + Aggregate *aggregate = sections.aggregate(); + startSection("obsolete", "Obsolete Members for " + aggregate->name()); + + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeStartElement(dbNamespace, "emphasis"); + m_writer->writeAttribute("role", "bold"); + m_writer->writeCharacters("The following members of QML type "); + generateSimpleLink(linkForNode(aggregate, nullptr), aggregate->name()); + m_writer->writeCharacters(" are deprecated."); + m_writer->writeEndElement(); // emphasis bold + m_writer->writeCharacters(" We strongly advise against using them in new code."); + m_writer->writeEndElement(); // para + newLine(); + + for (const auto *section : details_spv) { + const QString &title = "Obsolete " + section->title(); + startSection(title.toLower(), title); + + const NodeVector &members = section->obsoleteMembers(); + NodeVector::ConstIterator m = members.constBegin(); + while (m != members.constEnd()) { + if ((*m)->access() != Access::Private) + generateDetailedQmlMember(*m, aggregate); + ++m; + } + + endSection(); + } + + endSection(); +} + +static QString nodeToSynopsisTag(const Node *node) +{ + // Order from Node::nodeTypeString. + if (node->isClass() || node->isQmlType()) + return QStringLiteral("classsynopsis"); + if (node->isNamespace()) + return QStringLiteral("packagesynopsis"); + if (node->isPageNode()) { + node->doc().location().warning("Unexpected document node in nodeToSynopsisTag"); + return QString(); + } + if (node->isEnumType()) + return QStringLiteral("enumsynopsis"); + if (node->isTypedef()) + return QStringLiteral("typedefsynopsis"); + if (node->isFunction()) { + // Signals are also encoded as functions (including QML ones). + const auto fn = static_cast<const FunctionNode *>(node); + if (fn->isCtor() || fn->isCCtor() || fn->isMCtor()) + return QStringLiteral("constructorsynopsis"); + if (fn->isDtor()) + return QStringLiteral("destructorsynopsis"); + return QStringLiteral("methodsynopsis"); + } + if (node->isProperty() || node->isVariable() || node->isQmlProperty()) + return QStringLiteral("fieldsynopsis"); + + node->doc().location().warning(QString("Unknown node tag %1").arg(node->nodeTypeString())); + return QStringLiteral("synopsis"); +} + +void DocBookGenerator::generateStartRequisite(const QString &description) +{ + m_writer->writeStartElement(dbNamespace, "varlistentry"); + newLine(); + m_writer->writeTextElement(dbNamespace, "term", description); + newLine(); + m_writer->writeStartElement(dbNamespace, "listitem"); + newLine(); + m_writer->writeStartElement(dbNamespace, "para"); + m_inPara = true; +} + +void DocBookGenerator::generateEndRequisite() +{ + m_writer->writeEndElement(); // para + m_inPara = false; + newLine(); + m_writer->writeEndElement(); // listitem + newLine(); + m_writer->writeEndElement(); // varlistentry + newLine(); +} + +void DocBookGenerator::generateRequisite(const QString &description, const QString &value) +{ + generateStartRequisite(description); + m_writer->writeCharacters(value); + generateEndRequisite(); +} + +/*! + * \internal + * Generates the CMake (\a description) requisites + */ +void DocBookGenerator::generateCMakeRequisite(const QStringList &values) +{ + const QString description("CMake"); + generateStartRequisite(description); + m_writer->writeCharacters(values.first()); + m_writer->writeEndElement(); // para + newLine(); + + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeCharacters(values.last()); + generateEndRequisite(); +} + +void DocBookGenerator::generateSortedNames(const ClassNode *cn, const QList<RelatedClass> &rc) +{ + // From Generator::appendSortedNames. + QMap<QString, ClassNode *> classMap; + QList<RelatedClass>::ConstIterator r = rc.constBegin(); + while (r != rc.constEnd()) { + ClassNode *rcn = (*r).m_node; + if (rcn && rcn->access() == Access::Public && rcn->status() != Node::Internal + && !rcn->doc().isEmpty()) { + classMap[rcn->plainFullName(cn).toLower()] = rcn; + } + ++r; + } + + QStringList classNames = classMap.keys(); + classNames.sort(); + + int index = 0; + for (const QString &className : classNames) { + generateFullName(classMap.value(className), cn); + m_writer->writeCharacters(Utilities::comma(index++, classNames.size())); + } +} + +void DocBookGenerator::generateSortedQmlNames(const Node *base, const NodeList &subs) +{ + // From Generator::appendSortedQmlNames. + QMap<QString, Node *> classMap; + + for (auto sub : subs) + classMap[sub->plainFullName(base).toLower()] = sub; + + QStringList names = classMap.keys(); + names.sort(); + + int index = 0; + for (const QString &name : names) { + generateFullName(classMap.value(name), base); + m_writer->writeCharacters(Utilities::comma(index++, names.size())); + } +} + +/*! + Lists the required imports and includes. +*/ +void DocBookGenerator::generateRequisites(const Aggregate *aggregate) +{ + // Adapted from HtmlGenerator::generateRequisites, but simplified: no need to store all the + // elements, they can be produced one by one. + + // Generate the requisites first separately: if some of them are generated, output them in a wrapper. + // This complexity is required to ensure the DocBook file is valid: an empty list is not valid. It is not easy + // to write a truly comprehensive condition. + QXmlStreamWriter* oldWriter = m_writer; + QString output; + m_writer = new QXmlStreamWriter(&output); + + // Includes. + if (aggregate->includeFile()) generateRequisite("Header", *aggregate->includeFile()); + + // Since and project. + if (!aggregate->since().isEmpty()) + generateRequisite("Since", formatSince(aggregate)); + + if (aggregate->isClassNode() || aggregate->isNamespace()) { + // CMake and QT variable. + const CollectionNode *cn = + m_qdb->getCollectionNode(aggregate->physicalModuleName(), Node::Module); + if (cn && !cn->qtCMakeComponent().isEmpty()) { + const QString qtComponent = "Qt" + QString::number(QT_VERSION_MAJOR); + const QString findpackageText = "find_package(" + qtComponent + + " REQUIRED COMPONENTS " + cn->qtCMakeComponent() + ")"; + const QString targetItem = + cn->qtCMakeTargetItem().isEmpty() ? cn->qtCMakeComponent() : cn->qtCMakeTargetItem(); + const QString targetLinkLibrariesText = "target_link_libraries(mytarget PRIVATE " + + qtComponent + "::" + targetItem + ")"; + const QStringList cmakeInfo { findpackageText, targetLinkLibrariesText }; + generateCMakeRequisite(cmakeInfo); + } + if (cn && !cn->qtVariable().isEmpty()) + generateRequisite("qmake", "QT += " + cn->qtVariable()); + } + + if (aggregate->nodeType() == Node::Class) { + // Native type information. + auto *classe = const_cast<ClassNode *>(static_cast<const ClassNode *>(aggregate)); + if (classe && classe->isQmlNativeType() && classe->status() != Node::Internal) { + generateStartRequisite("In QML"); + + qsizetype idx{0}; + QList<QmlTypeNode *> nativeTypes { classe->qmlNativeTypes().cbegin(), classe->qmlNativeTypes().cend()}; + std::sort(nativeTypes.begin(), nativeTypes.end(), Node::nodeNameLessThan); + + for (const auto &item : std::as_const(nativeTypes)) { + generateFullName(item, classe); + m_writer->writeCharacters( + Utilities::comma(idx++, nativeTypes.size())); + } + generateEndRequisite(); + } + + // Inherits. + QList<RelatedClass>::ConstIterator r; + if (!classe->baseClasses().isEmpty()) { + generateStartRequisite("Inherits"); + + r = classe->baseClasses().constBegin(); + int index = 0; + while (r != classe->baseClasses().constEnd()) { + if ((*r).m_node) { + generateFullName((*r).m_node, classe); + + if ((*r).m_access == Access::Protected) + m_writer->writeCharacters(" (protected)"); + else if ((*r).m_access == Access::Private) + m_writer->writeCharacters(" (private)"); + m_writer->writeCharacters( + Utilities::comma(index++, classe->baseClasses().size())); + } + ++r; + } + + generateEndRequisite(); + } + + // Inherited by. + if (!classe->derivedClasses().isEmpty()) { + generateStartRequisite("Inherited By"); + generateSortedNames(classe, classe->derivedClasses()); + generateEndRequisite(); + } + } + + // Group. + if (!aggregate->groupNames().empty()) { + generateStartRequisite("Group"); + generateGroupReferenceText(aggregate); + generateEndRequisite(); + } + + // Status. + if (auto status = formatStatus(aggregate, m_qdb); status) + generateRequisite("Status", status.value()); + + // Write the elements as a list if not empty. + delete m_writer; + m_writer = oldWriter; + + if (!output.isEmpty()) { + // Namespaces are mangled in this output, because QXmlStreamWriter doesn't know about them. (Letting it know + // would imply generating the xmlns declaration one more time.) + static const QRegularExpression xmlTag(R"(<(/?)n\d+:)"); // Only for DocBook tags. + static const QRegularExpression xmlnsDocBookDefinition(R"( xmlns:n\d+=")" + QString{dbNamespace} + "\""); + static const QRegularExpression xmlnsXLinkDefinition(R"( xmlns:n\d+=")" + QString{xlinkNamespace} + "\""); + static const QRegularExpression xmlAttr(R"( n\d+:)"); // Only for XLink attributes. + // Space at the beginning! + const QString cleanOutput = output.replace(xmlTag, R"(<\1db:)") + .replace(xmlnsDocBookDefinition, "") + .replace(xmlnsXLinkDefinition, "") + .replace(xmlAttr, " xlink:"); + + m_writer->writeStartElement(dbNamespace, "variablelist"); + if (m_useITS) + m_writer->writeAttribute(itsNamespace, "translate", "no"); + newLine(); + + m_writer->device()->write(cleanOutput.toUtf8()); + + m_writer->writeEndElement(); // variablelist + newLine(); + } +} + +/*! + Lists the required imports and includes. +*/ +void DocBookGenerator::generateQmlRequisites(const QmlTypeNode *qcn) +{ + // From HtmlGenerator::generateQmlRequisites, but simplified: no need to store all the elements, + // they can be produced one by one. + if (!qcn) + return; + + const CollectionNode *collection = qcn->logicalModule(); + + NodeList subs; + QmlTypeNode::subclasses(qcn, subs); + + QmlTypeNode *base = qcn->qmlBaseNode(); + while (base && base->isInternal()) { + base = base->qmlBaseNode(); + } + + // Skip import statement for \internal collections + const bool generate_import_statement = !qcn->logicalModuleName().isEmpty() && (!collection || !collection->isInternal() || m_showInternal); + // Detect if anything is generated in this method. If not, exit early to avoid having an empty list. + const bool generates_something = generate_import_statement || !qcn->since().isEmpty() || !subs.isEmpty() || base; + + if (!generates_something) + return; + + // Start writing the elements as a list. + m_writer->writeStartElement(dbNamespace, "variablelist"); + if (m_useITS) + m_writer->writeAttribute(itsNamespace, "translate", "no"); + newLine(); + + if (generate_import_statement) { + QStringList parts = QStringList() << "import" << qcn->logicalModuleName() << qcn->logicalModuleVersion(); + generateRequisite("Import Statement", parts.join(' ').trimmed()); + } + + // Since and project. + if (!qcn->since().isEmpty()) + generateRequisite("Since:", formatSince(qcn)); + + // Inherited by. + if (!subs.isEmpty()) { + generateStartRequisite("Inherited By:"); + generateSortedQmlNames(qcn, subs); + generateEndRequisite(); + } + + // Inherits. + if (base) { + const Node *otherNode = nullptr; + Atom a = Atom(Atom::LinkNode, CodeMarker::stringForNode(base)); + QString link = getAutoLink(&a, qcn, &otherNode); + + generateStartRequisite("Inherits:"); + generateSimpleLink(link, base->name()); + generateEndRequisite(); + } + + // Native type information. + ClassNode *cn = (const_cast<QmlTypeNode *>(qcn))->classNode(); + if (cn && cn->isQmlNativeType() && cn->status() != Node::Internal) { + generateStartRequisite("In C++:"); + generateSimpleLink(fullDocumentLocation(cn), cn->name()); + generateEndRequisite(); + } + + // Group. + if (!qcn->groupNames().empty()) { + generateStartRequisite("Group"); + generateGroupReferenceText(qcn); + generateEndRequisite(); + } + + // Status. + if (auto status = formatStatus(qcn, m_qdb); status) + generateRequisite("Status:", status.value()); + + m_writer->writeEndElement(); // variablelist + newLine(); +} + +bool DocBookGenerator::generateStatus(const Node *node) +{ + // From Generator::generateStatus. + switch (node->status()) { + case Node::Active: + // Output the module 'state' description if set. + if (node->isModule() || node->isQmlModule()) { + const QString &state = static_cast<const CollectionNode*>(node)->state(); + if (!state.isEmpty()) { + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeCharacters("This " + typeString(node) + " is in "); + m_writer->writeStartElement(dbNamespace, "emphasis"); + m_writer->writeCharacters(state); + m_writer->writeEndElement(); // emphasis + m_writer->writeCharacters(" state."); + m_writer->writeEndElement(); // para + newLine(); + return true; + } + } + if (const auto version = node->deprecatedSince(); !version.isEmpty()) { + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeCharacters("This " + typeString(node) + + " is scheduled for deprecation in version " + + version + "."); + m_writer->writeEndElement(); // para + newLine(); + return true; + } + return false; + case Node::Preliminary: + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeStartElement(dbNamespace, "emphasis"); + m_writer->writeAttribute("role", "bold"); + m_writer->writeCharacters("This " + typeString(node) + + " is under development and is subject to change."); + m_writer->writeEndElement(); // emphasis + m_writer->writeEndElement(); // para + newLine(); + return true; + case Node::Deprecated: + m_writer->writeStartElement(dbNamespace, "para"); + if (node->isAggregate()) { + m_writer->writeStartElement(dbNamespace, "emphasis"); + m_writer->writeAttribute("role", "bold"); + } + m_writer->writeCharacters("This " + typeString(node) + " is deprecated"); + if (const QString &version = node->deprecatedSince(); !version.isEmpty()) { + m_writer->writeCharacters(" since "); + if (node->isQmlNode() && !node->logicalModuleName().isEmpty()) + m_writer->writeCharacters(node->logicalModuleName() + " "); + m_writer->writeCharacters(version); + } + m_writer->writeCharacters(". We strongly advise against using it in new code."); + if (node->isAggregate()) + m_writer->writeEndElement(); // emphasis + m_writer->writeEndElement(); // para + newLine(); + return true; + case Node::Internal: + default: + return false; + } +} + +/*! + Generate a list of function signatures. The function nodes + are in \a nodes. + */ +void DocBookGenerator::generateSignatureList(const NodeList &nodes) +{ + // From Generator::signatureList and Generator::appendSignature. + m_writer->writeStartElement(dbNamespace, "itemizedlist"); + newLine(); + + NodeList::ConstIterator n = nodes.constBegin(); + while (n != nodes.constEnd()) { + m_writer->writeStartElement(dbNamespace, "listitem"); + newLine(); + m_writer->writeStartElement(dbNamespace, "para"); + + generateSimpleLink(currentGenerator()->fullDocumentLocation(*n), + (*n)->signature(Node::SignaturePlain)); + + m_writer->writeEndElement(); // para + newLine(); + m_writer->writeEndElement(); // itemizedlist + newLine(); + ++n; + } + + m_writer->writeEndElement(); // itemizedlist + newLine(); +} + +/*! + * Return a string representing a text that exposes information about + * the groups that the \a node is part of. + */ +void DocBookGenerator::generateGroupReferenceText(const Node* node) +{ + // From HtmlGenerator::groupReferenceText + + if (!node->isAggregate()) + return; + const auto aggregate = static_cast<const Aggregate *>(node); + + const QStringList &groups_names{aggregate->groupNames()}; + if (!groups_names.empty()) { + m_writer->writeCharacters(aggregate->name() + " is part of "); + m_writer->writeStartElement(dbNamespace, "simplelist"); + + for (qsizetype index{0}; index < groups_names.size(); ++index) { + CollectionNode* group{m_qdb->groups()[groups_names[index]]}; + m_qdb->mergeCollections(group); + + m_writer->writeStartElement(dbNamespace, "member"); + if (QString target{linkForNode(group, nullptr)}; !target.isEmpty()) + generateSimpleLink(target, group->fullTitle()); + else + m_writer->writeCharacters(group->name()); + m_writer->writeEndElement(); // member + } + + m_writer->writeEndElement(); // simplelist + newLine(); + } +} + +/*! + Generates text that explains how threadsafe and/or reentrant + \a node is. + */ +bool DocBookGenerator::generateThreadSafeness(const Node *node) +{ + // From Generator::generateThreadSafeness + Node::ThreadSafeness ts = node->threadSafeness(); + + const Node *reentrantNode; + Atom reentrantAtom = Atom(Atom::Link, "reentrant"); + QString linkReentrant = getAutoLink(&reentrantAtom, node, &reentrantNode); + const Node *threadSafeNode; + Atom threadSafeAtom = Atom(Atom::Link, "thread-safe"); + QString linkThreadSafe = getAutoLink(&threadSafeAtom, node, &threadSafeNode); + + if (ts == Node::NonReentrant) { + m_writer->writeStartElement(dbNamespace, "warning"); + newLine(); + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeCharacters("This " + typeString(node) + " is not "); + generateSimpleLink(linkReentrant, "reentrant"); + m_writer->writeCharacters("."); + m_writer->writeEndElement(); // para + newLine(); + m_writer->writeEndElement(); // warning + + return true; + } else if (ts == Node::Reentrant || ts == Node::ThreadSafe) { + m_writer->writeStartElement(dbNamespace, "note"); + newLine(); + m_writer->writeStartElement(dbNamespace, "para"); + + if (node->isAggregate()) { + m_writer->writeCharacters("All functions in this " + typeString(node) + " are "); + if (ts == Node::ThreadSafe) + generateSimpleLink(linkThreadSafe, "thread-safe"); + else + generateSimpleLink(linkReentrant, "reentrant"); + + NodeList reentrant; + NodeList threadsafe; + NodeList nonreentrant; + bool exceptions = hasExceptions(node, reentrant, threadsafe, nonreentrant); + if (!exceptions || (ts == Node::Reentrant && !threadsafe.isEmpty())) { + m_writer->writeCharacters("."); + m_writer->writeEndElement(); // para + newLine(); + } else { + m_writer->writeCharacters(" with the following exceptions:"); + m_writer->writeEndElement(); // para + newLine(); + m_writer->writeStartElement(dbNamespace, "para"); + + if (ts == Node::Reentrant) { + if (!nonreentrant.isEmpty()) { + m_writer->writeCharacters("These functions are not "); + generateSimpleLink(linkReentrant, "reentrant"); + m_writer->writeCharacters(":"); + m_writer->writeEndElement(); // para + newLine(); + generateSignatureList(nonreentrant); + } + if (!threadsafe.isEmpty()) { + m_writer->writeCharacters("These functions are also "); + generateSimpleLink(linkThreadSafe, "thread-safe"); + m_writer->writeCharacters(":"); + m_writer->writeEndElement(); // para + newLine(); + generateSignatureList(threadsafe); + } + } else { // thread-safe + if (!reentrant.isEmpty()) { + m_writer->writeCharacters("These functions are only "); + generateSimpleLink(linkReentrant, "reentrant"); + m_writer->writeCharacters(":"); + m_writer->writeEndElement(); // para + newLine(); + generateSignatureList(reentrant); + } + if (!nonreentrant.isEmpty()) { + m_writer->writeCharacters("These functions are not "); + generateSimpleLink(linkReentrant, "reentrant"); + m_writer->writeCharacters(":"); + m_writer->writeEndElement(); // para + newLine(); + generateSignatureList(nonreentrant); + } + } + } + } else { + m_writer->writeCharacters("This " + typeString(node) + " is "); + if (ts == Node::ThreadSafe) + generateSimpleLink(linkThreadSafe, "thread-safe"); + else + generateSimpleLink(linkReentrant, "reentrant"); + m_writer->writeCharacters("."); + m_writer->writeEndElement(); // para + newLine(); + } + m_writer->writeEndElement(); // note + newLine(); + + return true; + } + + return false; +} + +/*! + Generate the body of the documentation from the qdoc comment + found with the entity represented by the \a node. + */ +void DocBookGenerator::generateBody(const Node *node) +{ + // From Generator::generateBody, without warnings. + const FunctionNode *fn = node->isFunction() ? static_cast<const FunctionNode *>(node) : nullptr; + + if (!node->hasDoc()) { + /* + Test for special function, like a destructor or copy constructor, + that has no documentation. + */ + if (fn) { + QString t; + if (fn->isDtor()) { + t = "Destroys the instance of " + fn->parent()->name() + "."; + if (fn->isVirtual()) + t += " The destructor is virtual."; + } else if (fn->isCtor()) { + t = "Default constructs an instance of " + fn->parent()->name() + "."; + } else if (fn->isCCtor()) { + t = "Copy constructor."; + } else if (fn->isMCtor()) { + t = "Move-copy constructor."; + } else if (fn->isCAssign()) { + t = "Copy-assignment constructor."; + } else if (fn->isMAssign()) { + t = "Move-assignment constructor."; + } + + if (!t.isEmpty()) + m_writer->writeTextElement(dbNamespace, "para", t); + } + } else if (!node->isSharingComment()) { + // Reimplements clause and type alias info precede body text + if (fn && !fn->overridesThis().isEmpty()) + generateReimplementsClause(fn); + else if (node->isProperty()) { + if (static_cast<const PropertyNode *>(node)->propertyType() != PropertyNode::PropertyType::StandardProperty) + generateAddendum(node, BindableProperty, nullptr, false); + } + + // Generate the body. + if (!generateText(node->doc().body(), node)) { + if (node->isMarkedReimp()) + return; + } + + // Output what is after the main body. + if (fn) { + if (fn->isQmlSignal()) + generateAddendum(node, QmlSignalHandler, nullptr, true); + if (fn->isPrivateSignal()) + generateAddendum(node, PrivateSignal, nullptr, true); + if (fn->isInvokable()) + generateAddendum(node, Invokable, nullptr, true); + if (fn->hasAssociatedProperties()) + generateAddendum(node, AssociatedProperties, nullptr, true); + } + + // Warning generation skipped with respect to Generator::generateBody. + } + + generateEnumValuesForQmlProperty(node, nullptr); + generateRequiredLinks(node); +} + +/*! + Generates either a link to the project folder for example \a node, or a list + of links files/images if 'url.examples config' variable is not defined. + + Does nothing for non-example nodes. +*/ +void DocBookGenerator::generateRequiredLinks(const Node *node) +{ + // From Generator::generateRequiredLinks. + if (!node->isExample()) + return; + + const auto en = static_cast<const ExampleNode *>(node); + QString exampleUrl{Config::instance().get(CONFIG_URL + Config::dot + CONFIG_EXAMPLES).asString()}; + + if (exampleUrl.isEmpty()) { + if (!en->noAutoList()) { + generateFileList(en, false); // files + generateFileList(en, true); // images + } + } else { + generateLinkToExample(en, exampleUrl); + } +} + +/*! + The path to the example replaces a placeholder '\1' character if + one is found in the \a baseUrl string. If no such placeholder is found, + the path is appended to \a baseUrl, after a '/' character if \a baseUrl did + not already end in one. +*/ +void DocBookGenerator::generateLinkToExample(const ExampleNode *en, const QString &baseUrl) +{ + // From Generator::generateLinkToExample. + QString exampleUrl(baseUrl); + QString link; +#ifndef QT_BOOTSTRAPPED + link = QUrl(exampleUrl).host(); +#endif + if (!link.isEmpty()) + link.prepend(" @ "); + link.prepend("Example project"); + + const QLatin1Char separator('/'); + const QLatin1Char placeholder('\1'); + if (!exampleUrl.contains(placeholder)) { + if (!exampleUrl.endsWith(separator)) + exampleUrl += separator; + exampleUrl += placeholder; + } + + // Construct a path to the example; <install path>/<example name> + QStringList path = QStringList() + << Config::instance().get(CONFIG_EXAMPLESINSTALLPATH).asString() << en->name(); + path.removeAll(QString()); + + // Write the link to the example. Typically, this link comes after sections, hence + // wrap it in a section too. + startSection("Example project"); + + m_writer->writeStartElement(dbNamespace, "para"); + generateSimpleLink(exampleUrl.replace(placeholder, path.join(separator)), link); + m_writer->writeEndElement(); // para + newLine(); + + endSection(); +} + +// TODO: [multi-purpose-function-with-flag][generate-file-list] + +/*! + This function is called when the documentation for an example is + being formatted. It outputs a list of files for the example, which + can be the example's source files or the list of images used by the + example. The images are copied into a subtree of + \c{...doc/html/images/used-in-examples/...} +*/ +void DocBookGenerator::generateFileList(const ExampleNode *en, bool images) +{ + // TODO: [possibly-stale-duplicate-code][generator-insufficient-structural-abstraction] + // Review and compare this code with + // Generator::generateFileList. + // Some subtle changes that might be semantically equivalent are + // present between the two. + // Supposedly, this version is to be considered stale compared to + // Generator's one and it might be possible to remove it in favor + // of that as long as the difference in output are taken into consideration. + + // From Generator::generateFileList + QString tag; + QStringList paths; + if (images) { + paths = en->images(); + tag = "Images:"; + } else { // files + paths = en->files(); + tag = "Files:"; + } + std::sort(paths.begin(), paths.end(), Generator::comparePaths); + + if (paths.isEmpty()) + return; + + startSection("", "List of Files"); + + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeCharacters(tag); + m_writer->writeEndElement(); // para + newLine(); + + startSection("List of Files"); + + m_writer->writeStartElement(dbNamespace, "itemizedlist"); + newLine(); + + for (const auto &path : std::as_const(paths)) { + auto maybe_resolved_file{file_resolver.resolve(path)}; + if (!maybe_resolved_file) { + // TODO: [uncentralized-admonition][failed-resolve-file] + QString details = std::transform_reduce( + file_resolver.get_search_directories().cbegin(), + file_resolver.get_search_directories().cend(), + u"Searched directories:"_s, + std::plus(), + [](const DirectoryPath &directory_path) -> QString { return u' ' + directory_path.value(); } + ); + + en->location().warning(u"Cannot find file to quote from: %1"_s.arg(path), details); + + continue; + } + + auto file{*maybe_resolved_file}; + if (images) addImageToCopy(en, file); + else generateExampleFilePage(en, file); + + m_writer->writeStartElement(dbNamespace, "listitem"); + newLine(); + m_writer->writeStartElement(dbNamespace, "para"); + generateSimpleLink(file.get_query(), file.get_query()); + m_writer->writeEndElement(); // para + m_writer->writeEndElement(); // listitem + newLine(); + } + + m_writer->writeEndElement(); // itemizedlist + newLine(); + + endSection(); +} + +/*! + Generate a file with the contents of a C++ or QML source file. + */ +void DocBookGenerator::generateExampleFilePage(const Node *node, ResolvedFile resolved_file, CodeMarker*) +{ + // TODO: [generator-insufficient-structural-abstraction] + + // From HtmlGenerator::generateExampleFilePage. + if (!node->isExample()) + return; + + // TODO: Understand if this is safe. + const auto en = static_cast<const ExampleNode *>(node); + + // Store current (active) writer + QXmlStreamWriter *currentWriter = m_writer; + m_writer = startDocument(en, resolved_file.get_query()); + generateHeader(en->fullTitle(), en->subtitle(), en); + + Text text; + Quoter quoter; + Doc::quoteFromFile(en->doc().location(), quoter, resolved_file); + QString code = quoter.quoteTo(en->location(), QString(), QString()); + CodeMarker *codeMarker = CodeMarker::markerForFileName(resolved_file.get_path()); + text << Atom(codeMarker->atomType(), code); + Atom a(codeMarker->atomType(), code); + generateText(text, en); + + endDocument(); // Delete m_writer. + m_writer = currentWriter; // Restore writer. +} + +void DocBookGenerator::generateReimplementsClause(const FunctionNode *fn) +{ + // From Generator::generateReimplementsClause, without warning generation. + if (fn->overridesThis().isEmpty() || !fn->parent()->isClassNode()) + return; + + auto cn = static_cast<ClassNode *>(fn->parent()); + + if (const FunctionNode *overrides = cn->findOverriddenFunction(fn); + overrides && !overrides->isPrivate() && !overrides->parent()->isPrivate()) { + if (overrides->hasDoc()) { + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeCharacters("Reimplements: "); + QString fullName = + overrides->parent()->name() + "::" + overrides->signature(Node::SignaturePlain); + generateFullName(overrides->parent(), fullName, overrides); + m_writer->writeCharacters("."); + m_writer->writeEndElement(); // para + newLine(); + return; + } + } + + if (const PropertyNode *sameName = cn->findOverriddenProperty(fn); sameName && sameName->hasDoc()) { + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeCharacters("Reimplements an access function for property: "); + QString fullName = sameName->parent()->name() + "::" + sameName->name(); + generateFullName(sameName->parent(), fullName, sameName); + m_writer->writeCharacters("."); + m_writer->writeEndElement(); // para + newLine(); + return; + } +} + +void DocBookGenerator::generateAlsoList(const Node *node) +{ + // From Generator::generateAlsoList. + QList<Text> alsoList = node->doc().alsoList(); + supplementAlsoList(node, alsoList); + + if (!alsoList.isEmpty()) { + startSection("See Also"); + + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeStartElement(dbNamespace, "emphasis"); + m_writer->writeCharacters("See also "); + m_writer->writeEndElement(); // emphasis + newLine(); + + m_writer->writeStartElement(dbNamespace, "simplelist"); + m_writer->writeAttribute("type", "vert"); + m_writer->writeAttribute("role", "see-also"); + newLine(); + + for (const Text &text : alsoList) { + m_writer->writeStartElement(dbNamespace, "member"); + generateText(text, node); + m_writer->writeEndElement(); // member + newLine(); + } + + m_writer->writeEndElement(); // simplelist + newLine(); + + m_writer->writeEndElement(); // para + newLine(); + + endSection(); + } +} + +/*! + Open a new file to write XML contents, including the DocBook + opening tag. + */ +QXmlStreamWriter *DocBookGenerator::startGenericDocument(const Node *node, const QString &fileName) +{ + QFile *outFile = openSubPageFile(node, fileName); + m_writer = new QXmlStreamWriter(outFile); + m_writer->setAutoFormatting(false); // We need a precise handling of line feeds. + + m_writer->writeStartDocument(); + newLine(); + m_writer->writeNamespace(dbNamespace, "db"); + m_writer->writeNamespace(xlinkNamespace, "xlink"); + if (m_useITS) + m_writer->writeNamespace(itsNamespace, "its"); + m_writer->writeStartElement(dbNamespace, "article"); + m_writer->writeAttribute("version", "5.2"); + if (!m_naturalLanguage.isEmpty()) + m_writer->writeAttribute("xml:lang", m_naturalLanguage); + newLine(); + + // Reset the state for the new document. + sectionLevels.resize(0); + m_inPara = false; + m_inList = 0; + + return m_writer; +} + +QXmlStreamWriter *DocBookGenerator::startDocument(const Node *node) +{ + m_hasSection = false; + refMap.clear(); + + QString fileName = Generator::fileName(node, fileExtension()); + return startGenericDocument(node, fileName); +} + +QXmlStreamWriter *DocBookGenerator::startDocument(const ExampleNode *en, const QString &file) +{ + m_hasSection = false; + + QString fileName = linkForExampleFile(file); + return startGenericDocument(en, fileName); +} + +void DocBookGenerator::endDocument() +{ + m_writer->writeEndElement(); // article + m_writer->writeEndDocument(); + + m_writer->device()->close(); + delete m_writer->device(); + delete m_writer; + m_writer = nullptr; +} + +/*! + Generate a reference page for the C++ class, namespace, or + header file documented in \a node. + */ +void DocBookGenerator::generateCppReferencePage(Node *node) +{ + // Based on HtmlGenerator::generateCppReferencePage. + Q_ASSERT(node->isAggregate()); + const auto aggregate = static_cast<const Aggregate *>(node); + + QString title; + QString rawTitle; + QString fullTitle; + if (aggregate->isNamespace()) { + rawTitle = aggregate->plainName(); + fullTitle = aggregate->plainFullName(); + title = rawTitle + " Namespace"; + } else if (aggregate->isClass()) { + rawTitle = aggregate->plainName(); + + auto templateDecl = node->templateDecl(); + if (templateDecl) + fullTitle = QString("%1 %2 ").arg((*templateDecl).to_qstring(), aggregate->typeWord(false)); + + fullTitle += aggregate->plainFullName(); + title = rawTitle + QLatin1Char(' ') + aggregate->typeWord(true); + } else if (aggregate->isHeader()) { + title = fullTitle = rawTitle = aggregate->fullTitle(); + } + + QString subtitleText; + if (rawTitle != fullTitle) + subtitleText = fullTitle; + + // Start producing the DocBook file. + m_writer = startDocument(node); + + // Info container. + generateHeader(title, subtitleText, aggregate); + + generateRequisites(aggregate); + generateStatus(aggregate); + + // Element synopsis. + generateDocBookSynopsis(node); + + // Actual content. + if (!aggregate->doc().isEmpty()) { + startSection("details", "Detailed Description"); + + generateBody(aggregate); + generateAlsoList(aggregate); + + endSection(); + } + + Sections sections(const_cast<Aggregate *>(aggregate)); + SectionVector sectionVector = + (aggregate->isNamespace() || aggregate->isHeader()) ? + sections.stdDetailsSections() : + sections.stdCppClassDetailsSections(); + for (const Section §ion : sectionVector) { + if (section.members().isEmpty()) + continue; + + startSection(section.title().toLower(), section.title()); + + for (const Node *member : section.members()) { + if (member->access() == Access::Private) // ### check necessary? + continue; + + if (member->nodeType() != Node::Class) { + // This function starts its own section. + generateDetailedMember(member, aggregate); + } else { + startSectionBegin(); + m_writer->writeCharacters("class "); + generateFullName(member, aggregate); + startSectionEnd(); + + generateBrief(member); + + endSection(); + } + } + + endSection(); + } + + generateObsoleteMembers(sections); + + endDocument(); +} + +void DocBookGenerator::generateSynopsisInfo(const QString &key, const QString &value) +{ + m_writer->writeStartElement(dbNamespace, "synopsisinfo"); + m_writer->writeAttribute("role", key); + m_writer->writeCharacters(value); + m_writer->writeEndElement(); // synopsisinfo + newLine(); +} + +void DocBookGenerator::generateModifier(const QString &value) +{ + m_writer->writeTextElement(dbNamespace, "modifier", value); + newLine(); +} + +/*! + Generate the metadata for the given \a node in DocBook. + */ +void DocBookGenerator::generateDocBookSynopsis(const Node *node) +{ + if (!node) + return; + + // From Generator::generateStatus, HtmlGenerator::generateRequisites, + // Generator::generateThreadSafeness, QDocIndexFiles::generateIndexSection. + + // This function is the major place where DocBook extensions are used. + if (!m_useDocBook52) + return; + + // Nothing to export in some cases. Note that isSharedCommentNode() returns + // true also for QML property groups. + if (node->isGroup() || node->isSharedCommentNode() || node->isModule() || node->isQmlModule() || node->isPageNode()) + return; + + // Cast the node to several subtypes (null pointer if the node is not of the required type). + const Aggregate *aggregate = + node->isAggregate() ? static_cast<const Aggregate *>(node) : nullptr; + const ClassNode *classNode = node->isClass() ? static_cast<const ClassNode *>(node) : nullptr; + const FunctionNode *functionNode = + node->isFunction() ? static_cast<const FunctionNode *>(node) : nullptr; + const PropertyNode *propertyNode = + node->isProperty() ? static_cast<const PropertyNode *>(node) : nullptr; + const VariableNode *variableNode = + node->isVariable() ? static_cast<const VariableNode *>(node) : nullptr; + const EnumNode *enumNode = node->isEnumType() ? static_cast<const EnumNode *>(node) : nullptr; + const QmlPropertyNode *qpn = + node->isQmlProperty() ? static_cast<const QmlPropertyNode *>(node) : nullptr; + const QmlTypeNode *qcn = node->isQmlType() ? static_cast<const QmlTypeNode *>(node) : nullptr; + // Typedefs are ignored, as they correspond to enums. + // Groups and modules are ignored. + // Documents are ignored, they have no interesting metadata. + + // Start the synopsis tag. + QString synopsisTag = nodeToSynopsisTag(node); + m_writer->writeStartElement(dbNamespace, synopsisTag); + newLine(); + + // Name and basic properties of each tag (like types and parameters). + if (node->isClass()) { + m_writer->writeStartElement(dbNamespace, "ooclass"); + m_writer->writeTextElement(dbNamespace, "classname", node->plainName()); + m_writer->writeEndElement(); // ooclass + newLine(); + } else if (node->isNamespace()) { + m_writer->writeTextElement(dbNamespace, "namespacename", node->plainName()); + newLine(); + } else if (node->isQmlType()) { + m_writer->writeStartElement(dbNamespace, "ooclass"); + m_writer->writeTextElement(dbNamespace, "classname", node->plainName()); + m_writer->writeEndElement(); // ooclass + newLine(); + if (!qcn->groupNames().isEmpty()) + m_writer->writeAttribute("groups", qcn->groupNames().join(QLatin1Char(','))); + } else if (node->isProperty()) { + m_writer->writeTextElement(dbNamespace, "modifier", "(Qt property)"); + newLine(); + m_writer->writeTextElement(dbNamespace, "type", propertyNode->dataType()); + newLine(); + m_writer->writeTextElement(dbNamespace, "varname", node->plainName()); + newLine(); + } else if (node->isVariable()) { + if (variableNode->isStatic()) { + m_writer->writeTextElement(dbNamespace, "modifier", "static"); + newLine(); + } + m_writer->writeTextElement(dbNamespace, "type", variableNode->dataType()); + newLine(); + m_writer->writeTextElement(dbNamespace, "varname", node->plainName()); + newLine(); + } else if (node->isEnumType()) { + m_writer->writeTextElement(dbNamespace, "enumname", node->plainName()); + newLine(); + } else if (node->isQmlProperty()) { + QString name = node->name(); + if (qpn->isAttached()) + name.prepend(qpn->element() + QLatin1Char('.')); + + m_writer->writeTextElement(dbNamespace, "type", qpn->dataType()); + newLine(); + m_writer->writeTextElement(dbNamespace, "varname", name); + newLine(); + + if (qpn->isAttached()) { + m_writer->writeTextElement(dbNamespace, "modifier", "attached"); + newLine(); + } + if (!(const_cast<QmlPropertyNode *>(qpn))->isReadOnly()) { + m_writer->writeTextElement(dbNamespace, "modifier", "writable"); + newLine(); + } + if ((const_cast<QmlPropertyNode *>(qpn))->isRequired()) { + m_writer->writeTextElement(dbNamespace, "modifier", "required"); + newLine(); + } + if (qpn->isReadOnly()) { + generateModifier("[read-only]"); + newLine(); + } + if (qpn->isDefault()) { + generateModifier("[default]"); + newLine(); + } + } else if (node->isFunction()) { + if (functionNode->virtualness() != "non") + generateModifier("virtual"); + if (functionNode->isConst()) + generateModifier("const"); + if (functionNode->isStatic()) + generateModifier("static"); + + if (!functionNode->isMacro() && !functionNode->isCtor() && + !functionNode->isCCtor() && !functionNode->isMCtor() + && !functionNode->isDtor()) { + if (functionNode->returnType() == "void") + m_writer->writeEmptyElement(dbNamespace, "void"); + else + m_writer->writeTextElement(dbNamespace, "type", functionNode->returnType()); + newLine(); + } + // Remove two characters from the plain name to only get the name + // of the method without parentheses (only for functions, not macros). + QString name = node->plainName(); + if (name.endsWith("()")) + name.chop(2); + m_writer->writeTextElement(dbNamespace, "methodname", name); + newLine(); + + if (functionNode->parameters().isEmpty()) { + m_writer->writeEmptyElement(dbNamespace, "void"); + newLine(); + } + + const Parameters &lp = functionNode->parameters(); + for (int i = 0; i < lp.count(); ++i) { + const Parameter ¶meter = lp.at(i); + m_writer->writeStartElement(dbNamespace, "methodparam"); + newLine(); + m_writer->writeTextElement(dbNamespace, "type", parameter.type()); + newLine(); + m_writer->writeTextElement(dbNamespace, "parameter", parameter.name()); + newLine(); + if (!parameter.defaultValue().isEmpty()) { + m_writer->writeTextElement(dbNamespace, "initializer", parameter.defaultValue()); + newLine(); + } + m_writer->writeEndElement(); // methodparam + newLine(); + } + + if (functionNode->isDefault()) + generateModifier("default"); + if (functionNode->isFinal()) + generateModifier("final"); + if (functionNode->isOverride()) + generateModifier("override"); + } else if (node->isTypedef()) { + m_writer->writeTextElement(dbNamespace, "typedefname", node->plainName()); + newLine(); + } else { + node->doc().location().warning( + QStringLiteral("Unexpected node type in generateDocBookSynopsis: %1") + .arg(node->nodeTypeString())); + newLine(); + } + + // Enums and typedefs. + if (enumNode) { + for (const EnumItem &item : enumNode->items()) { + m_writer->writeStartElement(dbNamespace, "enumitem"); + newLine(); + m_writer->writeTextElement(dbNamespace, "enumidentifier", item.name()); + newLine(); + m_writer->writeTextElement(dbNamespace, "enumvalue", item.value()); + newLine(); + m_writer->writeEndElement(); // enumitem + newLine(); + } + + if (enumNode->items().isEmpty()) { + // If the enumeration is empty (really rare case), still produce + // something for the DocBook document to be valid. + m_writer->writeStartElement(dbNamespace, "enumitem"); + newLine(); + m_writer->writeEmptyElement(dbNamespace, "enumidentifier"); + newLine(); + m_writer->writeEndElement(); // enumitem + newLine(); + } + } + + // Below: only synopsisinfo within synopsisTag. These elements must be at + // the end of the tag, as per DocBook grammar. + + // Information for functions that could not be output previously + // (synopsisinfo). + if (node->isFunction()) { + generateSynopsisInfo("meta", functionNode->metanessString()); + + if (functionNode->isOverload()) { + generateSynopsisInfo("overload", "overload"); + generateSynopsisInfo("overload-number", + QString::number(functionNode->overloadNumber())); + } + + if (functionNode->isRef()) + generateSynopsisInfo("refness", QString::number(1)); + else if (functionNode->isRefRef()) + generateSynopsisInfo("refness", QString::number(2)); + + if (functionNode->hasAssociatedProperties()) { + QStringList associatedProperties; + const auto &nodes = functionNode->associatedProperties(); + for (const Node *n : nodes) { + const auto pn = static_cast<const PropertyNode *>(n); + associatedProperties << pn->name(); + } + associatedProperties.sort(); + generateSynopsisInfo("associated-property", + associatedProperties.join(QLatin1Char(','))); + } + + QString signature = functionNode->signature(Node::SignatureReturnType); + // 'const' is already part of FunctionNode::signature() + if (functionNode->isFinal()) + signature += " final"; + if (functionNode->isOverride()) + signature += " override"; + if (functionNode->isPureVirtual()) + signature += " = 0"; + else if (functionNode->isDefault()) + signature += " = default"; + generateSynopsisInfo("signature", signature); + } + + // Accessibility status. + if (!node->isPageNode() && !node->isCollectionNode()) { + switch (node->access()) { + case Access::Public: + generateSynopsisInfo("access", "public"); + break; + case Access::Protected: + generateSynopsisInfo("access", "protected"); + break; + case Access::Private: + generateSynopsisInfo("access", "private"); + break; + default: + break; + } + if (node->isAbstract()) + generateSynopsisInfo("abstract", "true"); + } + + // Status. + switch (node->status()) { + case Node::Active: + generateSynopsisInfo("status", "active"); + break; + case Node::Preliminary: + generateSynopsisInfo("status", "preliminary"); + break; + case Node::Deprecated: + generateSynopsisInfo("status", "deprecated"); + break; + case Node::Internal: + generateSynopsisInfo("status", "internal"); + break; + default: + generateSynopsisInfo("status", "main"); + break; + } + + // C++ classes and name spaces. + if (aggregate) { + // Includes. + if (aggregate->includeFile()) generateSynopsisInfo("headers", *aggregate->includeFile()); + + // Since and project. + if (!aggregate->since().isEmpty()) + generateSynopsisInfo("since", formatSince(aggregate)); + + if (aggregate->nodeType() == Node::Class || aggregate->nodeType() == Node::Namespace) { + // CMake and QT variable. + if (!aggregate->physicalModuleName().isEmpty()) { + const CollectionNode *cn = + m_qdb->getCollectionNode(aggregate->physicalModuleName(), Node::Module); + if (cn && !cn->qtCMakeComponent().isEmpty()) { + const QString qtComponent = "Qt" + QString::number(QT_VERSION_MAJOR); + const QString findpackageText = "find_package(" + qtComponent + + " REQUIRED COMPONENTS " + cn->qtCMakeComponent() + ")"; + const QString targetLinkLibrariesText = + "target_link_libraries(mytarget PRIVATE " + qtComponent + "::" + cn->qtCMakeComponent() + + ")"; + generateSynopsisInfo("cmake-find-package", findpackageText); + generateSynopsisInfo("cmake-target-link-libraries", targetLinkLibrariesText); + } + if (cn && !cn->qtVariable().isEmpty()) + generateSynopsisInfo("qmake", "QT += " + cn->qtVariable()); + } + } + + if (aggregate->nodeType() == Node::Class) { + // Native type + auto *classe = const_cast<ClassNode *>(static_cast<const ClassNode *>(aggregate)); + if (classe && classe->isQmlNativeType() && classe->status() != Node::Internal) { + m_writer->writeStartElement(dbNamespace, "synopsisinfo"); + m_writer->writeAttribute("role", "nativeTypeFor"); + + QList<QmlTypeNode *> nativeTypes { classe->qmlNativeTypes().cbegin(), classe->qmlNativeTypes().cend()}; + std::sort(nativeTypes.begin(), nativeTypes.end(), Node::nodeNameLessThan); + + for (auto item : std::as_const(nativeTypes)) { + const Node *otherNode{nullptr}; + Atom a = Atom(Atom::LinkNode, CodeMarker::stringForNode(item)); + const QString &link = getAutoLink(&a, aggregate, &otherNode); + generateSimpleLink(link, item->name()); + } + + m_writer->writeEndElement(); // synopsisinfo + } + + // Inherits. + QList<RelatedClass>::ConstIterator r; + if (!classe->baseClasses().isEmpty()) { + m_writer->writeStartElement(dbNamespace, "synopsisinfo"); + m_writer->writeAttribute("role", "inherits"); + + r = classe->baseClasses().constBegin(); + int index = 0; + while (r != classe->baseClasses().constEnd()) { + if ((*r).m_node) { + generateFullName((*r).m_node, classe); + + if ((*r).m_access == Access::Protected) { + m_writer->writeCharacters(" (protected)"); + } else if ((*r).m_access == Access::Private) { + m_writer->writeCharacters(" (private)"); + } + m_writer->writeCharacters( + Utilities::comma(index++, classe->baseClasses().size())); + } + ++r; + } + + m_writer->writeEndElement(); // synopsisinfo + newLine(); + } + + // Inherited by. + if (!classe->derivedClasses().isEmpty()) { + m_writer->writeStartElement(dbNamespace, "synopsisinfo"); + m_writer->writeAttribute("role", "inheritedBy"); + generateSortedNames(classe, classe->derivedClasses()); + m_writer->writeEndElement(); // synopsisinfo + newLine(); + } + } + } + + // QML types. + if (qcn) { + // Module name and version (i.e. import). + QString logicalModuleVersion; + const CollectionNode *collection = + m_qdb->getCollectionNode(qcn->logicalModuleName(), qcn->nodeType()); + if (collection) + logicalModuleVersion = collection->logicalModuleVersion(); + else + logicalModuleVersion = qcn->logicalModuleVersion(); + + QStringList importText; + importText << "import " + qcn->logicalModuleName(); + if (!logicalModuleVersion.isEmpty()) + importText << logicalModuleVersion; + generateSynopsisInfo("import", importText.join(' ')); + + // Since and project. + if (!qcn->since().isEmpty()) + generateSynopsisInfo("since", formatSince(qcn)); + + // Inherited by. + NodeList subs; + QmlTypeNode::subclasses(qcn, subs); + if (!subs.isEmpty()) { + m_writer->writeTextElement(dbNamespace, "synopsisinfo"); + m_writer->writeAttribute("role", "inheritedBy"); + generateSortedQmlNames(qcn, subs); + m_writer->writeEndElement(); // synopsisinfo + newLine(); + } + + // Inherits. + QmlTypeNode *base = qcn->qmlBaseNode(); + while (base && base->isInternal()) + base = base->qmlBaseNode(); + if (base) { + const Node *otherNode = nullptr; + Atom a = Atom(Atom::LinkNode, CodeMarker::stringForNode(base)); + QString link = getAutoLink(&a, base, &otherNode); + + m_writer->writeTextElement(dbNamespace, "synopsisinfo"); + m_writer->writeAttribute("role", "inherits"); + generateSimpleLink(link, base->name()); + m_writer->writeEndElement(); // synopsisinfo + newLine(); + } + + // Native type + ClassNode *cn = (const_cast<QmlTypeNode *>(qcn))->classNode(); + + if (cn && cn->isQmlNativeType() && (cn->status() != Node::Internal)) { + const Node *otherNode = nullptr; + Atom a = Atom(Atom::LinkNode, CodeMarker::stringForNode(qcn)); + QString link = getAutoLink(&a, cn, &otherNode); + + m_writer->writeTextElement(dbNamespace, "synopsisinfo"); + m_writer->writeAttribute("role", "nativeType"); + generateSimpleLink(link, cn->name()); + m_writer->writeEndElement(); // synopsisinfo + newLine(); + } + } + + // Thread safeness. + switch (node->threadSafeness()) { + case Node::UnspecifiedSafeness: + generateSynopsisInfo("threadsafeness", "unspecified"); + break; + case Node::NonReentrant: + generateSynopsisInfo("threadsafeness", "non-reentrant"); + break; + case Node::Reentrant: + generateSynopsisInfo("threadsafeness", "reentrant"); + break; + case Node::ThreadSafe: + generateSynopsisInfo("threadsafeness", "thread safe"); + break; + default: + generateSynopsisInfo("threadsafeness", "unspecified"); + break; + } + + // Module. + if (!node->physicalModuleName().isEmpty()) + generateSynopsisInfo("module", node->physicalModuleName()); + + // Group. + if (classNode && !classNode->groupNames().isEmpty()) { + generateSynopsisInfo("groups", classNode->groupNames().join(QLatin1Char(','))); + } else if (qcn && !qcn->groupNames().isEmpty()) { + generateSynopsisInfo("groups", qcn->groupNames().join(QLatin1Char(','))); + } + + // Properties. + if (propertyNode) { + for (const Node *fnNode : propertyNode->getters()) { + if (fnNode) { + const auto funcNode = static_cast<const FunctionNode *>(fnNode); + generateSynopsisInfo("getter", funcNode->name()); + } + } + for (const Node *fnNode : propertyNode->setters()) { + if (fnNode) { + const auto funcNode = static_cast<const FunctionNode *>(fnNode); + generateSynopsisInfo("setter", funcNode->name()); + } + } + for (const Node *fnNode : propertyNode->resetters()) { + if (fnNode) { + const auto funcNode = static_cast<const FunctionNode *>(fnNode); + generateSynopsisInfo("resetter", funcNode->name()); + } + } + for (const Node *fnNode : propertyNode->notifiers()) { + if (fnNode) { + const auto funcNode = static_cast<const FunctionNode *>(fnNode); + generateSynopsisInfo("notifier", funcNode->name()); + } + } + } + + m_writer->writeEndElement(); // nodeToSynopsisTag (like classsynopsis) + newLine(); + + // The typedef associated to this enum. It is output *after* the main tag, + // i.e. it must be after the synopsisinfo. + if (enumNode && enumNode->flagsType()) { + m_writer->writeStartElement(dbNamespace, "typedefsynopsis"); + newLine(); + + m_writer->writeTextElement(dbNamespace, "typedefname", + enumNode->flagsType()->fullDocumentName()); + newLine(); + + m_writer->writeEndElement(); // typedefsynopsis + newLine(); + } +} + +QString taggedNode(const Node *node) +{ + // From CodeMarker::taggedNode, but without the tag part (i.e. only the QML specific case + // remaining). + // TODO: find a better name for this. + if (node->nodeType() == Node::QmlType && node->name().startsWith(QLatin1String("QML:"))) + return node->name().mid(4); + return node->name(); +} + +/*! + Parses a string with method/variable name and (return) type + to include type tags. + */ +void DocBookGenerator::typified(const QString &string, const Node *relative, bool trailingSpace, + bool generateType) +{ + // Adapted from CodeMarker::typified and HtmlGenerator::highlightedCode. + // Note: CppCodeMarker::markedUpIncludes is not needed for DocBook, as this part is natively + // generated as DocBook. Hence, there is no need to reimplement <@headerfile> from + // HtmlGenerator::highlightedCode. + QString result; + QString pendingWord; + + for (int i = 0; i <= string.size(); ++i) { + QChar ch; + if (i != string.size()) + ch = string.at(i); + + QChar lower = ch.toLower(); + if ((lower >= QLatin1Char('a') && lower <= QLatin1Char('z')) || ch.digitValue() >= 0 + || ch == QLatin1Char('_') || ch == QLatin1Char(':')) { + pendingWord += ch; + } else { + if (!pendingWord.isEmpty()) { + bool isProbablyType = (pendingWord != QLatin1String("const")); + if (generateType && isProbablyType) { + // Flush the current buffer. + m_writer->writeCharacters(result); + result.truncate(0); + + // Add the link, logic from HtmlGenerator::highlightedCode. + const Node *n = m_qdb->findTypeNode(pendingWord, relative, Node::DontCare); + QString href; + if (!(n && n->isQmlBasicType()) + || (relative + && (relative->genus() == n->genus() || Node::DontCare == n->genus()))) { + href = linkForNode(n, relative); + } + + m_writer->writeStartElement(dbNamespace, "type"); + if (href.isEmpty()) + m_writer->writeCharacters(pendingWord); + else + generateSimpleLink(href, pendingWord); + m_writer->writeEndElement(); // type + } else { + result += pendingWord; + } + } + pendingWord.clear(); + + if (ch.unicode() != '\0') + result += ch; + } + } + + if (trailingSpace && string.size()) { + if (!string.endsWith(QLatin1Char('*')) && !string.endsWith(QLatin1Char('&'))) + result += QLatin1Char(' '); + } + + m_writer->writeCharacters(result); +} + +void DocBookGenerator::generateSynopsisName(const Node *node, const Node *relative, + bool generateNameLink) +{ + // Implements the rewriting of <@link> from HtmlGenerator::highlightedCode, only due to calls to + // CodeMarker::linkTag in CppCodeMarker::markedUpSynopsis. + QString name = taggedNode(node); + + if (!generateNameLink) { + m_writer->writeCharacters(name); + return; + } + + m_writer->writeStartElement(dbNamespace, "emphasis"); + m_writer->writeAttribute("role", "bold"); + generateSimpleLink(linkForNode(node, relative), name); + m_writer->writeEndElement(); // emphasis +} + +void DocBookGenerator::generateParameter(const Parameter ¶meter, const Node *relative, + bool generateExtra, bool generateType) +{ + const QString &pname = parameter.name(); + const QString &ptype = parameter.type(); + QString paramName; + if (!pname.isEmpty()) { + typified(ptype, relative, true, generateType); + paramName = pname; + } else { + paramName = ptype; + } + + if (generateExtra || pname.isEmpty()) { + m_writer->writeStartElement(dbNamespace, "emphasis"); + m_writer->writeCharacters(paramName); + m_writer->writeEndElement(); // emphasis + } + + const QString &pvalue = parameter.defaultValue(); + if (generateExtra && !pvalue.isEmpty()) + m_writer->writeCharacters(" = " + pvalue); +} + +void DocBookGenerator::generateSynopsis(const Node *node, const Node *relative, + Section::Style style) +{ + // From HtmlGenerator::generateSynopsis (conditions written as booleans). + const bool generateExtra = style != Section::AllMembers; + const bool generateType = style != Section::Details; + const bool generateNameLink = style != Section::Details; + + // From CppCodeMarker::markedUpSynopsis, reversed the generation of "extra" and "synopsis". + const int MaxEnumValues = 6; + + if (generateExtra) { + if (auto extra = CodeMarker::extraSynopsis(node, style); !extra.isEmpty()) + m_writer->writeCharacters(extra + " "); + } + + // Then generate the synopsis. + QString namePrefix {}; + if (style == Section::Details) { + if (!node->isRelatedNonmember() && !node->isProxyNode() && !node->parent()->name().isEmpty() + && !node->parent()->isHeader() && !node->isProperty() && !node->isQmlNode()) { + namePrefix = taggedNode(node->parent()) + "::"; + } + } + + switch (node->nodeType()) { + case Node::Namespace: + m_writer->writeCharacters("namespace "); + m_writer->writeCharacters(namePrefix); + generateSynopsisName(node, relative, generateNameLink); + break; + case Node::Class: + m_writer->writeCharacters("class "); + m_writer->writeCharacters(namePrefix); + generateSynopsisName(node, relative, generateNameLink); + break; + case Node::Function: { + const auto func = (const FunctionNode *)node; + + // First, the part coming before the name. + if (style == Section::Summary || style == Section::Accessors) { + if (!func->isNonvirtual()) + m_writer->writeCharacters(QStringLiteral("virtual ")); + } + + // Name and parameters. + if (style != Section::AllMembers && !func->returnType().isEmpty()) + typified(func->returnType(), relative, true, generateType); + m_writer->writeCharacters(namePrefix); + generateSynopsisName(node, relative, generateNameLink); + + if (!func->isMacroWithoutParams()) { + m_writer->writeCharacters(QStringLiteral("(")); + if (!func->parameters().isEmpty()) { + const Parameters ¶meters = func->parameters(); + for (int i = 0; i < parameters.count(); i++) { + if (i > 0) + m_writer->writeCharacters(QStringLiteral(", ")); + generateParameter(parameters.at(i), relative, generateExtra, generateType); + } + } + m_writer->writeCharacters(QStringLiteral(")")); + } + + if (func->isConst()) + m_writer->writeCharacters(QStringLiteral(" const")); + + if (style == Section::Summary || style == Section::Accessors) { + // virtual is prepended, if needed. + QString synopsis; + if (func->isFinal()) + synopsis += QStringLiteral(" final"); + if (func->isOverride()) + synopsis += QStringLiteral(" override"); + if (func->isPureVirtual()) + synopsis += QStringLiteral(" = 0"); + if (func->isRef()) + synopsis += QStringLiteral(" &"); + else if (func->isRefRef()) + synopsis += QStringLiteral(" &&"); + m_writer->writeCharacters(synopsis); + } else if (style == Section::AllMembers) { + if (!func->returnType().isEmpty() && func->returnType() != "void") { + m_writer->writeCharacters(QStringLiteral(" : ")); + typified(func->returnType(), relative, false, generateType); + } + } else { + QString synopsis; + if (func->isRef()) + synopsis += QStringLiteral(" &"); + else if (func->isRefRef()) + synopsis += QStringLiteral(" &&"); + m_writer->writeCharacters(synopsis); + } + } break; + case Node::Enum: { + const auto enume = static_cast<const EnumNode *>(node); + m_writer->writeCharacters(QStringLiteral("enum ")); + m_writer->writeCharacters(namePrefix); + generateSynopsisName(node, relative, generateNameLink); + + QString synopsis; + if (style == Section::Summary) { + synopsis += " { "; + + QStringList documentedItems = enume->doc().enumItemNames(); + if (documentedItems.isEmpty()) { + const auto &enumItems = enume->items(); + for (const auto &item : enumItems) + documentedItems << item.name(); + } + const QStringList omitItems = enume->doc().omitEnumItemNames(); + for (const auto &item : omitItems) + documentedItems.removeAll(item); + + if (documentedItems.size() > MaxEnumValues) { + // Take the last element and keep it safe, then elide the surplus. + const QString last = documentedItems.last(); + documentedItems = documentedItems.mid(0, MaxEnumValues - 1); + documentedItems += "…"; // Ellipsis: in HTML, …. + documentedItems += last; + } + synopsis += documentedItems.join(QLatin1String(", ")); + + if (!documentedItems.isEmpty()) + synopsis += QLatin1Char(' '); + synopsis += QLatin1Char('}'); + } + m_writer->writeCharacters(synopsis); + } break; + case Node::TypeAlias: { + if (style == Section::Details) { + auto templateDecl = node->templateDecl(); + if (templateDecl) + m_writer->writeCharacters((*templateDecl).to_qstring() + QLatin1Char(' ')); + } + m_writer->writeCharacters(namePrefix); + generateSynopsisName(node, relative, generateNameLink); + } break; + case Node::Typedef: { + if (static_cast<const TypedefNode *>(node)->associatedEnum()) + m_writer->writeCharacters("flags "); + m_writer->writeCharacters(namePrefix); + generateSynopsisName(node, relative, generateNameLink); + } break; + case Node::Property: { + const auto property = static_cast<const PropertyNode *>(node); + m_writer->writeCharacters(namePrefix); + generateSynopsisName(node, relative, generateNameLink); + m_writer->writeCharacters(" : "); + typified(property->qualifiedDataType(), relative, false, generateType); + } break; + case Node::Variable: { + const auto variable = static_cast<const VariableNode *>(node); + if (style == Section::AllMembers) { + generateSynopsisName(node, relative, generateNameLink); + m_writer->writeCharacters(" : "); + typified(variable->dataType(), relative, false, generateType); + } else { + typified(variable->leftType(), relative, false, generateType); + m_writer->writeCharacters(" "); + m_writer->writeCharacters(namePrefix); + generateSynopsisName(node, relative, generateNameLink); + m_writer->writeCharacters(variable->rightType()); + } + } break; + default: + m_writer->writeCharacters(namePrefix); + generateSynopsisName(node, relative, generateNameLink); + } +} + +void DocBookGenerator::generateEnumValue(const QString &enumValue, const Node *relative) +{ + // From CppCodeMarker::markedUpEnumValue, simplifications from Generator::plainCode (removing + // <@op>). With respect to CppCodeMarker::markedUpEnumValue, the order of generation of parents + // must be reversed so that they are processed in the order + const auto *node = relative->parent(); + + if (relative->isQmlProperty()) { + const auto *qpn = static_cast<const QmlPropertyNode*>(relative); + if (qpn->enumNode() && !enumValue.startsWith("%1."_L1.arg(qpn->enumPrefix()))) { + m_writer->writeCharacters("%1.%2"_L1.arg(qpn->enumPrefix(), enumValue)); + return; + } + } + + if (!relative->isEnumType()) { + m_writer->writeCharacters(enumValue); + return; + } + + QList<const Node *> parents; + while (!node->isHeader() && node->parent()) { + parents.prepend(node); + if (node->parent() == relative || node->parent()->name().isEmpty()) + break; + node = node->parent(); + } + if (static_cast<const EnumNode *>(relative)->isScoped()) + parents << relative; + + m_writer->writeStartElement(dbNamespace, "code"); + for (auto parent : parents) { + generateSynopsisName(parent, relative, true); + m_writer->writeCharacters("::"); + } + + m_writer->writeCharacters(enumValue); + m_writer->writeEndElement(); // code +} + +/*! + If the node is an overloaded signal, and a node with an + example on how to connect to it + + Someone didn't finish writing this comment, and I don't know what this + function is supposed to do, so I have not tried to complete the comment + yet. + */ +void DocBookGenerator::generateOverloadedSignal(const Node *node) +{ + // From Generator::generateOverloadedSignal. + QString code = getOverloadedSignalCode(node); + if (code.isEmpty()) + return; + + m_writer->writeStartElement(dbNamespace, "note"); + newLine(); + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeCharacters("Signal "); + m_writer->writeTextElement(dbNamespace, "emphasis", node->name()); + m_writer->writeCharacters(" is overloaded in this class. To connect to this " + "signal by using the function pointer syntax, Qt " + "provides a convenient helper for obtaining the " + "function pointer as shown in this example:"); + m_writer->writeTextElement(dbNamespace, "code", code); + m_writer->writeEndElement(); // para + newLine(); + m_writer->writeEndElement(); // note + newLine(); +} + +/*! + Generates an addendum note of type \a type for \a node. \a marker + is unused in this generator. +*/ +void DocBookGenerator::generateAddendum(const Node *node, Addendum type, CodeMarker *marker, + bool generateNote) +{ + Q_UNUSED(marker) + Q_ASSERT(node && !node->name().isEmpty()); + if (generateNote) { + m_writer->writeStartElement(dbNamespace, "note"); + newLine(); + } + switch (type) { + case Invokable: + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeCharacters( + "This function can be invoked via the meta-object system and from QML. See "); + generateSimpleLink(node->url(), "Q_INVOKABLE"); + m_writer->writeCharacters("."); + m_writer->writeEndElement(); // para + newLine(); + break; + case PrivateSignal: + m_writer->writeTextElement( + dbNamespace, "para", + "This is a private signal. It can be used in signal connections but " + "cannot be emitted by the user."); + break; + case QmlSignalHandler: + { + QString handler(node->name()); + int prefixLocation = handler.lastIndexOf('.', -2) + 1; + handler[prefixLocation] = handler[prefixLocation].toTitleCase(); + handler.insert(prefixLocation, QLatin1String("on")); + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeCharacters("The corresponding handler is "); + m_writer->writeTextElement(dbNamespace, "code", handler); + m_writer->writeCharacters("."); + m_writer->writeEndElement(); // para + newLine(); + break; + } + case AssociatedProperties: + { + if (!node->isFunction()) + return; + const auto *fn = static_cast<const FunctionNode *>(node); + auto propertyNodes = fn->associatedProperties(); + if (propertyNodes.isEmpty()) + return; + std::sort(propertyNodes.begin(), propertyNodes.end(), Node::nodeNameLessThan); + for (const auto propertyNode : std::as_const(propertyNodes)) { + QString msg; + const auto pn = static_cast<const PropertyNode *>(propertyNode); + switch (pn->role(fn)) { + case PropertyNode::FunctionRole::Getter: + msg = QStringLiteral("Getter function"); + break; + case PropertyNode::FunctionRole::Setter: + msg = QStringLiteral("Setter function"); + break; + case PropertyNode::FunctionRole::Resetter: + msg = QStringLiteral("Resetter function"); + break; + case PropertyNode::FunctionRole::Notifier: + msg = QStringLiteral("Notifier signal"); + break; + default: + continue; + } + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeCharacters(msg + " for property "); + generateSimpleLink(linkForNode(pn, nullptr), pn->name()); + m_writer->writeCharacters(". "); + m_writer->writeEndElement(); // para + newLine(); + } + break; + } + case BindableProperty: + { + const Node *linkNode; + Atom linkAtom = Atom(Atom::Link, "QProperty"); + QString link = getAutoLink(&linkAtom, node, &linkNode); + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeCharacters("This property supports "); + generateSimpleLink(link, "QProperty"); + m_writer->writeCharacters(" bindings."); + m_writer->writeEndElement(); // para + newLine(); + break; + } + default: + break; + } + + if (generateNote) { + m_writer->writeEndElement(); // note + newLine(); + } +} + +void DocBookGenerator::generateDetailedMember(const Node *node, const PageNode *relative) +{ + // From HtmlGenerator::generateDetailedMember. + bool closeSupplementarySection = false; + + if (node->isSharedCommentNode()) { + const auto *scn = reinterpret_cast<const SharedCommentNode *>(node); + const QList<Node *> &collective = scn->collective(); + + bool firstFunction = true; + for (const auto *sharedNode : collective) { + if (firstFunction) { + startSectionBegin(sharedNode); + } else { + m_writer->writeStartElement(dbNamespace, "bridgehead"); + m_writer->writeAttribute("renderas", "sect2"); + writeXmlId(sharedNode); + } + if (m_useITS) + m_writer->writeAttribute(itsNamespace, "translate", "no"); + + generateSynopsis(sharedNode, relative, Section::Details); + + if (firstFunction) { + startSectionEnd(); + firstFunction = false; + } else { + m_writer->writeEndElement(); // bridgehead + newLine(); + } + } + } else { + const EnumNode *etn; + if (node->isEnumType() && (etn = static_cast<const EnumNode *>(node))->flagsType()) { + startSectionBegin(node); + if (m_useITS) + m_writer->writeAttribute(itsNamespace, "translate", "no"); + generateSynopsis(etn, relative, Section::Details); + startSectionEnd(); + + m_writer->writeStartElement(dbNamespace, "bridgehead"); + m_writer->writeAttribute("renderas", "sect2"); + generateSynopsis(etn->flagsType(), relative, Section::Details); + m_writer->writeEndElement(); // bridgehead + newLine(); + } else { + startSectionBegin(node); + if (m_useITS) + m_writer->writeAttribute(itsNamespace, "translate", "no"); + generateSynopsis(node, relative, Section::Details); + startSectionEnd(); + } + } + Q_ASSERT(m_hasSection); + + generateDocBookSynopsis(node); + + generateStatus(node); + generateBody(node); + + // If the body ends with a section, the rest of the description must be wrapped in a section too. + if (node->hasDoc() && node->doc().body().firstAtom() && node->doc().body().lastAtom()->type() == Atom::SectionRight) { + closeSupplementarySection = true; + startSection("", "Notes"); + } + + generateOverloadedSignal(node); + generateComparisonCategory(node); + generateThreadSafeness(node); + generateSince(node); + + if (node->isProperty()) { + const auto property = static_cast<const PropertyNode *>(node); + if (property->propertyType() == PropertyNode::PropertyType::StandardProperty) { + Section section("", "", "", "", Section::Accessors); + + section.appendMembers(property->getters().toVector()); + section.appendMembers(property->setters().toVector()); + section.appendMembers(property->resetters().toVector()); + + if (!section.members().isEmpty()) { + m_writer->writeStartElement(dbNamespace, "para"); + newLine(); + m_writer->writeStartElement(dbNamespace, "emphasis"); + m_writer->writeAttribute("role", "bold"); + m_writer->writeCharacters("Access functions:"); + newLine(); + m_writer->writeEndElement(); // emphasis + newLine(); + m_writer->writeEndElement(); // para + newLine(); + generateSectionList(section, node); + } + + Section notifiers("", "", "", "", Section::Accessors); + notifiers.appendMembers(property->notifiers().toVector()); + + if (!notifiers.members().isEmpty()) { + m_writer->writeStartElement(dbNamespace, "para"); + newLine(); + m_writer->writeStartElement(dbNamespace, "emphasis"); + m_writer->writeAttribute("role", "bold"); + m_writer->writeCharacters("Notifier signal:"); + newLine(); + m_writer->writeEndElement(); // emphasis + newLine(); + m_writer->writeEndElement(); // para + newLine(); + generateSectionList(notifiers, node); + } + } + } else if (node->isEnumType()) { + const auto en = static_cast<const EnumNode *>(node); + + if (m_qflagsHref.isEmpty()) { + Node *qflags = m_qdb->findClassNode(QStringList("QFlags")); + if (qflags) + m_qflagsHref = linkForNode(qflags, nullptr); + } + + if (en->flagsType()) { + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeCharacters("The "); + m_writer->writeStartElement(dbNamespace, "code"); + m_writer->writeCharacters(en->flagsType()->name()); + m_writer->writeEndElement(); // code + m_writer->writeCharacters(" type is a typedef for "); + m_writer->writeStartElement(dbNamespace, "code"); + generateSimpleLink(m_qflagsHref, "QFlags"); + m_writer->writeCharacters("<" + en->name() + ">. "); + m_writer->writeEndElement(); // code + m_writer->writeCharacters("It stores an OR combination of "); + m_writer->writeStartElement(dbNamespace, "code"); + m_writer->writeCharacters(en->name()); + m_writer->writeEndElement(); // code + m_writer->writeCharacters(" values."); + m_writer->writeEndElement(); // para + newLine(); + } + } + + if (closeSupplementarySection) + endSection(); + + // The list of linked pages is always in its own section. + generateAlsoList(node); + + // Close the section for this member. + endSection(); // section +} + +void DocBookGenerator::generateSectionList(const Section §ion, const Node *relative, + bool useObsoleteMembers) +{ + // From HtmlGenerator::generateSectionList, just generating a list (not tables). + const NodeVector &members = + (useObsoleteMembers ? section.obsoleteMembers() : section.members()); + if (!members.isEmpty()) { + bool hasPrivateSignals = false; + bool isInvokable = false; + + m_writer->writeStartElement(dbNamespace, "itemizedlist"); + if (m_useITS) + m_writer->writeAttribute(itsNamespace, "translate", "no"); + newLine(); + + NodeVector::ConstIterator m = members.constBegin(); + while (m != members.constEnd()) { + if ((*m)->access() == Access::Private) { + ++m; + continue; + } + + m_writer->writeStartElement(dbNamespace, "listitem"); + newLine(); + m_writer->writeStartElement(dbNamespace, "para"); + + // prefix no more needed. + generateSynopsis(*m, relative, section.style()); + if ((*m)->isFunction()) { + const auto fn = static_cast<const FunctionNode *>(*m); + if (fn->isPrivateSignal()) + hasPrivateSignals = true; + else if (fn->isInvokable()) + isInvokable = true; + } + + m_writer->writeEndElement(); // para + newLine(); + m_writer->writeEndElement(); // listitem + newLine(); + + ++m; + } + + m_writer->writeEndElement(); // itemizedlist + newLine(); + + if (hasPrivateSignals) + generateAddendum(relative, Generator::PrivateSignal, nullptr, true); + if (isInvokable) + generateAddendum(relative, Generator::Invokable, nullptr, true); + } + + if (!useObsoleteMembers && section.style() == Section::Summary + && !section.inheritedMembers().isEmpty()) { + m_writer->writeStartElement(dbNamespace, "itemizedlist"); + if (m_useITS) + m_writer->writeAttribute(itsNamespace, "translate", "no"); + newLine(); + + generateSectionInheritedList(section, relative); + + m_writer->writeEndElement(); // itemizedlist + newLine(); + } +} + +void DocBookGenerator::generateSectionInheritedList(const Section §ion, const Node *relative) +{ + // From HtmlGenerator::generateSectionInheritedList. + QList<std::pair<Aggregate *, int>>::ConstIterator p = section.inheritedMembers().constBegin(); + while (p != section.inheritedMembers().constEnd()) { + m_writer->writeStartElement(dbNamespace, "listitem"); + m_writer->writeCharacters(QString::number((*p).second) + u' '); + if ((*p).second == 1) + m_writer->writeCharacters(section.singular()); + else + m_writer->writeCharacters(section.plural()); + m_writer->writeCharacters(" inherited from "); + generateSimpleLink(fileName((*p).first) + '#' + + Generator::cleanRef(section.title().toLower()), + (*p).first->plainFullName(relative)); + ++p; + } +} + +/*! + Generate the DocBook page for an entity that doesn't map + to any underlying parsable C++ or QML element. + */ +void DocBookGenerator::generatePageNode(PageNode *pn) +{ + // From HtmlGenerator::generatePageNode, remove anything related to TOCs. + Q_ASSERT(m_writer == nullptr); + m_writer = startDocument(pn); + + generateHeader(pn->fullTitle(), pn->subtitle(), pn); + generateBody(pn); + generateAlsoList(pn); + generateFooter(); + + endDocument(); +} + +/*! + Generate the DocBook page for a QML type. \qcn is the QML type. + */ +void DocBookGenerator::generateQmlTypePage(QmlTypeNode *qcn) +{ + // From HtmlGenerator::generateQmlTypePage. + // Start producing the DocBook file. + Q_ASSERT(m_writer == nullptr); + m_writer = startDocument(qcn); + + Generator::setQmlTypeContext(qcn); + QString title = qcn->fullTitle(); + if (qcn->isQmlBasicType()) + title.append(" QML Value Type"); + else + title.append(" QML Type"); + // TODO: for ITS attribute, only apply translate="no" on qcn->fullTitle(), + // not its suffix (which should be translated). generateHeader doesn't + // allow this kind of input, the title isn't supposed to be structured. + // Ideally, do the same in HTML. + + generateHeader(title, qcn->subtitle(), qcn); + generateQmlRequisites(qcn); + generateStatus(qcn); + + startSection("details", "Detailed Description"); + generateBody(qcn); + + generateAlsoList(qcn); + + endSection(); + + Sections sections(qcn); + for (const auto §ion : sections.stdQmlTypeDetailsSections()) { + if (!section.isEmpty()) { + startSection(section.title().toLower(), section.title()); + + for (const auto &member : section.members()) + generateDetailedQmlMember(member, qcn); + + endSection(); + } + } + + generateObsoleteQmlMembers(sections); + + generateFooter(); + Generator::setQmlTypeContext(nullptr); + + endDocument(); +} + +/*! + Outputs the DocBook detailed documentation for a section + on a QML element reference page. + */ +void DocBookGenerator::generateDetailedQmlMember(Node *node, const Aggregate *relative) +{ + // From HtmlGenerator::generateDetailedQmlMember, with elements from + // CppCodeMarker::markedUpQmlItem and HtmlGenerator::generateQmlItem. + auto getQmlPropertyTitle = [&](QmlPropertyNode *n) { + QString title{CodeMarker::extraSynopsis(n, Section::Details)}; + if (!title.isEmpty()) + title += ' '_L1; + // Finalise generation of name, as per CppCodeMarker::markedUpQmlItem. + if (n->isAttached()) + title += n->element() + QLatin1Char('.'); + title += n->name() + " : " + n->dataType(); + + return title; + }; + + auto generateQmlMethodTitle = [&](Node *node) { + generateSynopsis(node, relative, Section::Details); + }; + + if (node->isPropertyGroup()) { + const auto *scn = static_cast<const SharedCommentNode *>(node); + + QString heading; + if (!scn->name().isEmpty()) + heading = scn->name() + " group"; + else + heading = node->name(); + startSection(scn, heading); + // This last call creates a title for this section. In other words, + // titles are forbidden for the rest of the section, hence the use of + // bridgehead. + + const QList<Node *> sharedNodes = scn->collective(); + for (const auto &sharedNode : sharedNodes) { + if (sharedNode->isQmlProperty()) { + auto *qpn = static_cast<QmlPropertyNode *>(sharedNode); + + m_writer->writeStartElement(dbNamespace, "bridgehead"); + m_writer->writeAttribute("renderas", "sect2"); + writeXmlId(qpn); + m_writer->writeCharacters(getQmlPropertyTitle(qpn)); + m_writer->writeEndElement(); // bridgehead + newLine(); + + generateDocBookSynopsis(qpn); + } + } + } else if (node->isQmlProperty()) { + auto qpn = static_cast<QmlPropertyNode *>(node); + startSection(qpn, getQmlPropertyTitle(qpn)); + generateDocBookSynopsis(qpn); + } else if (node->isSharedCommentNode()) { + const auto scn = reinterpret_cast<const SharedCommentNode *>(node); + const QList<Node *> &sharedNodes = scn->collective(); + + // In the section, generate a title for the first node, then bridgeheads for + // the next ones. + int i = 0; + for (const auto &sharedNode : sharedNodes) { + // Ignore this element if there is nothing to generate. + if (!sharedNode->isFunction(Node::QML) && !sharedNode->isQmlProperty()) { + continue; + } + + // Write the tag containing the title. + if (i == 0) { + startSectionBegin(sharedNode); + } else { + m_writer->writeStartElement(dbNamespace, "bridgehead"); + m_writer->writeAttribute("renderas", "sect2"); + } + + // Write the title. + if (sharedNode->isFunction(Node::QML)) + generateQmlMethodTitle(sharedNode); + else if (sharedNode->isQmlProperty()) + m_writer->writeCharacters( + getQmlPropertyTitle(static_cast<QmlPropertyNode *>(sharedNode))); + + // Complete the title and the synopsis. + if (i == 0) + startSectionEnd(); + else + m_writer->writeEndElement(); // bridgehead + generateDocBookSynopsis(sharedNode); + ++i; + } + + // If the list is empty, still generate a section. + if (i == 0) { + startSectionBegin(refForNode(node)); + + if (node->isFunction(Node::QML)) + generateQmlMethodTitle(node); + else if (node->isQmlProperty()) + m_writer->writeCharacters( + getQmlPropertyTitle(static_cast<QmlPropertyNode *>(node))); + + startSectionEnd(); + } + } else { // assume the node is a method/signal handler + startSectionBegin(node); + generateQmlMethodTitle(node); + startSectionEnd(); + } + + generateStatus(node); + generateBody(node); + generateThreadSafeness(node); + generateSince(node); + generateAlsoList(node); + + endSection(); +} + +/*! + Recursive writing of DocBook files from the root \a node. + */ +void DocBookGenerator::generateDocumentation(Node *node) +{ + // Mainly from Generator::generateDocumentation, with parts from + // Generator::generateDocumentation and WebXMLGenerator::generateDocumentation. + // Don't generate nodes that are already processed, or if they're not + // supposed to generate output, ie. external, index or images nodes. + if (!node->url().isNull()) + return; + if (node->isIndexNode()) + return; + if (node->isInternal() && !m_showInternal) + return; + if (node->isExternalPage()) + return; + + if (node->parent()) { + if (node->isCollectionNode()) { + /* + A collection node collects: groups, C++ modules, or QML + modules. Testing for a CollectionNode must be done + before testing for a TextPageNode because a + CollectionNode is a PageNode at this point. + + Don't output an HTML page for the collection node unless + the \group, \module, or \qmlmodule command was actually + seen by qdoc in the qdoc comment for the node. + + A key prerequisite in this case is the call to + mergeCollections(cn). We must determine whether this + group, module, or QML module has members in other + modules. We know at this point that cn's members list + contains only members in the current module. Therefore, + before outputting the page for cn, we must search for + members of cn in the other modules and add them to the + members list. + */ + auto cn = static_cast<CollectionNode *>(node); + if (cn->wasSeen()) { + m_qdb->mergeCollections(cn); + generateCollectionNode(cn); + } else if (cn->isGenericCollection()) { + // Currently used only for the module's related orphans page + // but can be generalized for other kinds of collections if + // other use cases pop up. + generateGenericCollectionPage(cn); + } + } else if (node->isTextPageNode()) { // Pages. + generatePageNode(static_cast<PageNode *>(node)); + } else if (node->isAggregate()) { // Aggregates. + if ((node->isClassNode() || node->isHeader() || node->isNamespace()) + && node->docMustBeGenerated()) { + generateCppReferencePage(static_cast<Aggregate *>(node)); + } else if (node->isQmlType()) { // Includes QML value types + generateQmlTypePage(static_cast<QmlTypeNode *>(node)); + } else if (node->isProxyNode()) { + generateProxyPage(static_cast<Aggregate *>(node)); + } + } + } + + if (node->isAggregate()) { + auto *aggregate = static_cast<Aggregate *>(node); + for (auto c : aggregate->childNodes()) { + if (node->isPageNode() && !node->isPrivate()) + generateDocumentation(c); + } + } +} + +void DocBookGenerator::generateProxyPage(Aggregate *aggregate) +{ + // Adapted from HtmlGenerator::generateProxyPage. + Q_ASSERT(aggregate->isProxyNode()); + + // Start producing the DocBook file. + Q_ASSERT(m_writer == nullptr); + m_writer = startDocument(aggregate); + + // Info container. + generateHeader(aggregate->plainFullName(), "", aggregate); + + // No element synopsis. + + // Actual content. + if (!aggregate->doc().isEmpty()) { + startSection("details", "Detailed Description"); + + generateBody(aggregate); + generateAlsoList(aggregate); + + endSection(); + } + + Sections sections(aggregate); + SectionVector *detailsSections = §ions.stdDetailsSections(); + + for (const auto §ion : std::as_const(*detailsSections)) { + if (section.isEmpty()) + continue; + + startSection(section.title().toLower(), section.title()); + + const QList<Node *> &members = section.members(); + for (const auto &member : members) { + if (!member->isPrivate()) { // ### check necessary? + if (!member->isClassNode()) { + generateDetailedMember(member, aggregate); + } else { + startSectionBegin(); + generateFullName(member, aggregate); + startSectionEnd(); + + generateBrief(member); + endSection(); + } + } + } + + endSection(); + } + + generateFooter(); + + endDocument(); +} + +/*! + Generate the HTML page for a group, module, or QML module. + */ +void DocBookGenerator::generateCollectionNode(CollectionNode *cn) +{ + // Adapted from HtmlGenerator::generateCollectionNode. + // Start producing the DocBook file. + Q_ASSERT(m_writer == nullptr); + m_writer = startDocument(cn); + + // Info container. + generateHeader(cn->fullTitle(), cn->subtitle(), cn); + + // Element synopsis. + generateDocBookSynopsis(cn); + + // Generate brief for C++ modules, status for all modules. + if (cn->genus() != Node::DOC && cn->genus() != Node::DontCare) { + if (cn->isModule()) + generateBrief(cn); + generateStatus(cn); + generateSince(cn); + } + + // Actual content. + if (cn->isModule()) { + if (!cn->noAutoList()) { + NodeMap nmm{cn->getMembers(Node::Namespace)}; + if (!nmm.isEmpty()) { + startSection("namespaces", "Namespaces"); + generateAnnotatedList(cn, nmm.values(), "namespaces"); + endSection(); + } + nmm = cn->getMembers([](const Node *n){ return n->isClassNode(); }); + if (!nmm.isEmpty()) { + startSection("classes", "Classes"); + generateAnnotatedList(cn, nmm.values(), "classes"); + endSection(); + } + } + } + + bool generatedTitle = false; + if (cn->isModule() && !cn->doc().briefText().isEmpty()) { + startSection("details", "Detailed Description"); + generatedTitle = true; + } + // The anchor is only needed if the node has a body. + else if ( + // generateBody generates something. + !cn->doc().body().isEmpty() || + // generateAlsoList generates something. + !cn->doc().alsoList().empty() || + // generateAnnotatedList generates something. + (!cn->noAutoList() && (cn->isGroup() || cn->isQmlModule()))) { + writeAnchor("details"); + } + + generateBody(cn); + generateAlsoList(cn); + + if (!cn->noAutoList() && (cn->isGroup() || cn->isQmlModule())) + generateAnnotatedList(cn, cn->members(), "members", AutoSection); + + if (generatedTitle) + endSection(); + + generateFooter(); + + endDocument(); +} + +/*! + Generate the HTML page for a generic collection. This is usually + a collection of C++ elements that are related to an element in + a different module. + */ +void DocBookGenerator::generateGenericCollectionPage(CollectionNode *cn) +{ + // Adapted from HtmlGenerator::generateGenericCollectionPage. + // TODO: factor out this code to generate a file name. + QString name = cn->name().toLower(); + name.replace(QChar(' '), QString("-")); + QString filename = cn->tree()->physicalModuleName() + "-" + name + "." + fileExtension(); + + // Start producing the DocBook file. + Q_ASSERT(m_writer == nullptr); + m_writer = startGenericDocument(cn, filename); + + // Info container. + generateHeader(cn->fullTitle(), cn->subtitle(), cn); + + // Element synopsis. + generateDocBookSynopsis(cn); + + // Actual content. + m_writer->writeStartElement(dbNamespace, "para"); + m_writer->writeCharacters("Each function or type documented here is related to a class or " + "namespace that is documented in a different module. The reference " + "page for that class or namespace will link to the function or type " + "on this page."); + m_writer->writeEndElement(); // para + + const CollectionNode *cnc = cn; + const QList<Node *> members = cn->members(); + for (const auto &member : members) + generateDetailedMember(member, cnc); + + generateFooter(); + + endDocument(); +} + +void DocBookGenerator::generateFullName(const Node *node, const Node *relative) +{ + Q_ASSERT(node); + Q_ASSERT(relative); + + // From Generator::appendFullName. + m_writer->writeStartElement(dbNamespace, "link"); + m_writer->writeAttribute(xlinkNamespace, "href", fullDocumentLocation(node)); + m_writer->writeAttribute(xlinkNamespace, "role", targetType(node)); + m_writer->writeCharacters(node->fullName(relative)); + m_writer->writeEndElement(); // link +} + +void DocBookGenerator::generateFullName(const Node *apparentNode, const QString &fullName, + const Node *actualNode) +{ + Q_ASSERT(apparentNode); + Q_ASSERT(actualNode); + + // From Generator::appendFullName. + m_writer->writeStartElement(dbNamespace, "link"); + m_writer->writeAttribute(xlinkNamespace, "href", fullDocumentLocation(actualNode)); + m_writer->writeAttribute("role", targetType(actualNode)); + m_writer->writeCharacters(fullName); + m_writer->writeEndElement(); // link +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/docbookgenerator.h b/src/qdoc/qdoc/src/qdoc/docbookgenerator.h new file mode 100644 index 000000000..6083fc34a --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/docbookgenerator.h @@ -0,0 +1,170 @@ +// Copyright (C) 2019 Thibaut Cuvelier +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef DOCBOOKGENERATOR_H +#define DOCBOOKGENERATOR_H + +#include "codemarker.h" +#include "config.h" +#include "xmlgenerator.h" +#include "filesystem/fileresolver.h" + +#include <QtCore/qhash.h> +#include <QtCore/qxmlstream.h> + +QT_BEGIN_NAMESPACE + +class Aggregate; +class ExampleNode; +class FunctionNode; + +class DocBookGenerator : public XmlGenerator +{ +public: + explicit DocBookGenerator(FileResolver& file_resolver); + + void initializeGenerator() override; + QString format() override; + +protected: + [[nodiscard]] QString fileExtension() const override; + void generateDocumentation(Node *node) override; + using Generator::generateCppReferencePage; + void generateCppReferencePage(Node *node); + using Generator::generatePageNode; + void generatePageNode(PageNode *pn); + using Generator::generateQmlTypePage; + void generateQmlTypePage(QmlTypeNode *qcn); + using Generator::generateCollectionNode; + void generateCollectionNode(CollectionNode *cn); + using Generator::generateGenericCollectionPage; + void generateGenericCollectionPage(CollectionNode *cn); + using Generator::generateProxyPage; + void generateProxyPage(Aggregate *aggregate); + + void generateList(const Node *relative, const QString &selector, + Qt::SortOrder sortOrder = Qt::AscendingOrder); + void generateHeader(const QString &title, const QString &subtitle, const Node *node); + void closeTextSections(); + void generateFooter(); + void generateDocBookSynopsis(const Node *node); + void generateRequisites(const Aggregate *inner); + void generateQmlRequisites(const QmlTypeNode *qcn); + void generateSortedNames(const ClassNode *cn, const QList<RelatedClass> &rc); + void generateSortedQmlNames(const Node *base, const NodeList &subs); + bool generateStatus(const Node *node); + void generateGroupReferenceText(const Node *node); + bool generateThreadSafeness(const Node *node); + bool generateSince(const Node *node); + void generateAddendum(const Node *node, Generator::Addendum type, CodeMarker *marker, + bool generateNote) override; + using Generator::generateBody; + void generateBody(const Node *node); + + bool generateText(const Text &text, const Node *relative) override; + qsizetype generateAtom(const Atom *atom, const Node *relative, CodeMarker*) override; + +private: + + enum GeneratedListType { Auto, AutoSection, ItemizedList }; + + QXmlStreamWriter *startDocument(const Node *node); + QXmlStreamWriter *startDocument(const ExampleNode *en, const QString &file); + QXmlStreamWriter *startGenericDocument(const Node *node, const QString &fileName); + void endDocument(); + + void generateAnnotatedList(const Node *relative, const NodeList &nodeList, + const QString &selector, GeneratedListType type = Auto, + Qt::SortOrder sortOrder = Qt::AscendingOrder); + void generateAnnotatedLists(const Node *relative, const NodeMultiMap &nmm, + const QString &selector); + void generateCompactList(const Node *relative, const NodeMultiMap &nmm, bool includeAlphabet, + const QString &commonPrefix, const QString &selector); + using Generator::generateFileList; + void generateFileList(const ExampleNode *en, bool images); + void generateObsoleteMembers(const Sections §ions); + void generateObsoleteQmlMembers(const Sections §ions); + void generateSectionList(const Section §ion, const Node *relative, + bool useObsoleteMembers = false); + void generateSectionInheritedList(const Section §ion, const Node *relative); + void generateSynopsisName(const Node *node, const Node *relative, bool generateNameLink); + void generateParameter(const Parameter ¶meter, const Node *relative, bool generateExtra, + bool generateType); + void generateSynopsis(const Node *node, const Node *relative, Section::Style style); + void generateEnumValue(const QString &enumValue, const Node *relative); + void generateDetailedMember(const Node *node, const PageNode *relative); + void generateDetailedQmlMember(Node *node, const Aggregate *relative); + + void generateFullName(const Node *node, const Node *relative); + void generateFullName(const Node *apparentNode, const QString &fullName, + const Node *actualNode); + void generateBrief(const Node *node); + void generateAlsoList(const Node *node) override; + void generateSignatureList(const NodeList &nodes); + void generateReimplementsClause(const FunctionNode *fn); + void generateClassHierarchy(const Node *relative, NodeMultiMap &classMap); + void generateFunctionIndex(const Node *relative); + void generateLegaleseList(const Node *relative); + void generateExampleFilePage(const Node *en, ResolvedFile resolved_file, CodeMarker* = nullptr) override; + void generateOverloadedSignal(const Node *node); + void generateRequiredLinks(const Node *node); + void generateLinkToExample(const ExampleNode *en, const QString &baseUrl); + + void typified(const QString &string, const Node *relative, bool trailingSpace = false, + bool generateType = true); + void generateLink(const Atom *atom); + void beginLink(const QString &link, const Node *node, const Node *relative); + void endLink(); + void writeXmlId(const QString &id); + void writeXmlId(const Node *node); + inline void newLine(); + void startSectionBegin(const QString &id = ""); + void startSectionBegin(const Node *node); + void startSectionEnd(); + void startSection(const QString &id, const QString &title); + void startSection(const Node *node, const QString &title); + void startSection(const QString &title); + void endSection(); + void writeAnchor(const QString &id); + void generateSimpleLink(const QString &href, const QString &text); + void generateStartRequisite(const QString &description); + void generateEndRequisite(); + void generateRequisite(const QString &description, const QString &value); + void generateCMakeRequisite(const QStringList &values); + void generateSynopsisInfo(const QString &key, const QString &value); + void generateModifier(const QString &value); + + // Generator state when outputting the documentation. + bool m_inListItemLineOpen { false }; + int currentSectionLevel { 0 }; + QStack<int> sectionLevels {}; + QString m_qflagsHref {}; + bool m_inTeletype { false }; + bool m_hasSection { false }; + bool m_closeSectionAfterGeneratedList { false }; + bool m_closeSectionAfterRawTitle { false }; + bool m_closeFigureWrapper { false }; + bool m_tableHeaderAlreadyOutput { false }; + bool m_closeTableRow { false }; + bool m_closeTableCell { false }; + std::pair<QString, QString> m_tableWidthAttr {}; + bool m_inPara { false }; // Ignores nesting of paragraphs (like list items). + bool m_inBlockquote { false }; + unsigned m_inList { 0 }; // Depth in number of nested lists. + + // Generator configuration, set before starting the generation. + QString m_project {}; + QString m_projectDescription {}; + QString m_naturalLanguage {}; + QString m_buildVersion {}; + QXmlStreamWriter *m_writer { nullptr }; + bool m_useDocBook52 { false }; // Enable tags from DocBook 5.2. Also called "extensions". + bool m_useITS { false }; // Enable ITS attributes for parts that should not be translated. + + Config *m_config { nullptr }; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/docparser.cpp b/src/qdoc/qdoc/src/qdoc/docparser.cpp new file mode 100644 index 000000000..3d78fe38a --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/docparser.cpp @@ -0,0 +1,2846 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +#include "docparser.h" + +#include "codemarker.h" +#include "doc.h" +#include "docprivate.h" +#include "editdistance.h" +#include "macro.h" +#include "openedlist.h" +#include "tokenizer.h" + +#include <QtCore/qfile.h> +#include <QtCore/qregularexpression.h> +#include <QtCore/qtextstream.h> + +#include <cctype> +#include <climits> +#include <functional> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +DocUtilities &DocParser::s_utilities = DocUtilities::instance(); + +enum { + CMD_A, + CMD_ANNOTATEDLIST, + CMD_B, + CMD_BADCODE, + CMD_BOLD, + CMD_BR, + CMD_BRIEF, + CMD_C, + CMD_CAPTION, + CMD_CODE, + CMD_CODELINE, + CMD_COMPARESWITH, + CMD_DETAILS, + CMD_DIV, + CMD_DOTS, + CMD_E, + CMD_ELSE, + CMD_ENDCODE, + CMD_ENDCOMPARESWITH, + CMD_ENDDETAILS, + CMD_ENDDIV, + CMD_ENDFOOTNOTE, + CMD_ENDIF, + CMD_ENDLEGALESE, + CMD_ENDLINK, + CMD_ENDLIST, + CMD_ENDMAPREF, + CMD_ENDOMIT, + CMD_ENDQUOTATION, + CMD_ENDRAW, + CMD_ENDSECTION1, + CMD_ENDSECTION2, + CMD_ENDSECTION3, + CMD_ENDSECTION4, + CMD_ENDSIDEBAR, + CMD_ENDTABLE, + CMD_FOOTNOTE, + CMD_GENERATELIST, + CMD_HEADER, + CMD_HR, + CMD_I, + CMD_IF, + CMD_IMAGE, + CMD_IMPORTANT, + CMD_INCLUDE, + CMD_INLINEIMAGE, + CMD_INDEX, + CMD_INPUT, + CMD_KEYWORD, + CMD_L, + CMD_LEGALESE, + CMD_LI, + CMD_LINK, + CMD_LIST, + CMD_META, + CMD_NOTE, + CMD_O, + CMD_OMIT, + CMD_OMITVALUE, + CMD_OVERLOAD, + CMD_PRINTLINE, + CMD_PRINTTO, + CMD_PRINTUNTIL, + CMD_QUOTATION, + CMD_QUOTEFILE, + CMD_QUOTEFROMFILE, + CMD_RAW, + CMD_ROW, + CMD_SA, + CMD_SECTION1, + CMD_SECTION2, + CMD_SECTION3, + CMD_SECTION4, + CMD_SIDEBAR, + CMD_SINCELIST, + CMD_SKIPLINE, + CMD_SKIPTO, + CMD_SKIPUNTIL, + CMD_SNIPPET, + CMD_SPAN, + CMD_SUB, + CMD_SUP, + CMD_TABLE, + CMD_TABLEOFCONTENTS, + CMD_TARGET, + CMD_TM, + CMD_TT, + CMD_UICONTROL, + CMD_UNDERLINE, + CMD_UNICODE, + CMD_VALUE, + CMD_WARNING, + CMD_QML, + CMD_ENDQML, + CMD_CPP, + CMD_ENDCPP, + CMD_CPPTEXT, + CMD_ENDCPPTEXT, + NOT_A_CMD +}; + +static struct +{ + const char *name; + int no; + bool is_formatting_command { false }; +} cmds[] = { { "a", CMD_A, true }, + { "annotatedlist", CMD_ANNOTATEDLIST }, + { "b", CMD_B, true }, + { "badcode", CMD_BADCODE }, + { "bold", CMD_BOLD, true }, + { "br", CMD_BR }, + { "brief", CMD_BRIEF }, + { "c", CMD_C, true }, + { "caption", CMD_CAPTION }, + { "code", CMD_CODE }, + { "codeline", CMD_CODELINE }, + { "compareswith", CMD_COMPARESWITH }, + { "details", CMD_DETAILS }, + { "div", CMD_DIV }, + { "dots", CMD_DOTS }, + { "e", CMD_E, true }, + { "else", CMD_ELSE }, + { "endcode", CMD_ENDCODE }, + { "endcompareswith", CMD_ENDCOMPARESWITH }, + { "enddetails", CMD_ENDDETAILS }, + { "enddiv", CMD_ENDDIV }, + { "endfootnote", CMD_ENDFOOTNOTE }, + { "endif", CMD_ENDIF }, + { "endlegalese", CMD_ENDLEGALESE }, + { "endlink", CMD_ENDLINK }, + { "endlist", CMD_ENDLIST }, + { "endmapref", CMD_ENDMAPREF }, + { "endomit", CMD_ENDOMIT }, + { "endquotation", CMD_ENDQUOTATION }, + { "endraw", CMD_ENDRAW }, + { "endsection1", CMD_ENDSECTION1 }, // ### don't document for now + { "endsection2", CMD_ENDSECTION2 }, // ### don't document for now + { "endsection3", CMD_ENDSECTION3 }, // ### don't document for now + { "endsection4", CMD_ENDSECTION4 }, // ### don't document for now + { "endsidebar", CMD_ENDSIDEBAR }, + { "endtable", CMD_ENDTABLE }, + { "footnote", CMD_FOOTNOTE }, + { "generatelist", CMD_GENERATELIST }, + { "header", CMD_HEADER }, + { "hr", CMD_HR }, + { "i", CMD_I, true }, + { "if", CMD_IF }, + { "image", CMD_IMAGE }, + { "important", CMD_IMPORTANT }, + { "include", CMD_INCLUDE }, + { "inlineimage", CMD_INLINEIMAGE }, + { "index", CMD_INDEX }, // ### don't document for now + { "input", CMD_INPUT }, + { "keyword", CMD_KEYWORD }, + { "l", CMD_L }, + { "legalese", CMD_LEGALESE }, + { "li", CMD_LI }, + { "link", CMD_LINK }, + { "list", CMD_LIST }, + { "meta", CMD_META }, + { "note", CMD_NOTE }, + { "o", CMD_O }, + { "omit", CMD_OMIT }, + { "omitvalue", CMD_OMITVALUE }, + { "overload", CMD_OVERLOAD }, + { "printline", CMD_PRINTLINE }, + { "printto", CMD_PRINTTO }, + { "printuntil", CMD_PRINTUNTIL }, + { "quotation", CMD_QUOTATION }, + { "quotefile", CMD_QUOTEFILE }, + { "quotefromfile", CMD_QUOTEFROMFILE }, + { "raw", CMD_RAW }, + { "row", CMD_ROW }, + { "sa", CMD_SA }, + { "section1", CMD_SECTION1 }, + { "section2", CMD_SECTION2 }, + { "section3", CMD_SECTION3 }, + { "section4", CMD_SECTION4 }, + { "sidebar", CMD_SIDEBAR }, + { "sincelist", CMD_SINCELIST }, + { "skipline", CMD_SKIPLINE }, + { "skipto", CMD_SKIPTO }, + { "skipuntil", CMD_SKIPUNTIL }, + { "snippet", CMD_SNIPPET }, + { "span", CMD_SPAN }, + { "sub", CMD_SUB, true }, + { "sup", CMD_SUP, true }, + { "table", CMD_TABLE }, + { "tableofcontents", CMD_TABLEOFCONTENTS }, + { "target", CMD_TARGET }, + { "tm", CMD_TM, true }, + { "tt", CMD_TT, true }, + { "uicontrol", CMD_UICONTROL, true }, + { "underline", CMD_UNDERLINE, true }, + { "unicode", CMD_UNICODE }, + { "value", CMD_VALUE }, + { "warning", CMD_WARNING }, + { "qml", CMD_QML }, + { "endqml", CMD_ENDQML }, + { "cpp", CMD_CPP }, + { "endcpp", CMD_ENDCPP }, + { "cpptext", CMD_CPPTEXT }, + { "endcpptext", CMD_ENDCPPTEXT }, + { nullptr, 0 } }; + +int DocParser::s_tabSize; +QStringList DocParser::s_ignoreWords; +bool DocParser::s_quoting = false; +FileResolver *DocParser::file_resolver{ nullptr }; +static void processComparesWithCommand(DocPrivate *priv, const Location &location); + +static QString cleanLink(const QString &link) +{ + qsizetype colonPos = link.indexOf(':'); + if ((colonPos == -1) || (!link.startsWith("file:") && !link.startsWith("mailto:"))) + return link; + return link.mid(colonPos + 1).simplified(); +} + +void DocParser::initialize(const Config &config, FileResolver &file_resolver) +{ + s_tabSize = config.get(CONFIG_TABSIZE).asInt(); + s_ignoreWords = config.get(CONFIG_IGNOREWORDS).asStringList(); + + int i = 0; + while (cmds[i].name) { + s_utilities.cmdHash.insert(cmds[i].name, cmds[i].no); + + if (cmds[i].no != i) + Location::internalError(QStringLiteral("command %1 missing").arg(i)); + ++i; + } + + // If any of the formats define quotinginformation, activate quoting + DocParser::s_quoting = config.get(CONFIG_QUOTINGINFORMATION).asBool(); + const auto &outputFormats = config.getOutputFormats(); + for (const auto &format : outputFormats) + DocParser::s_quoting = DocParser::s_quoting + || config.get(format + Config::dot + CONFIG_QUOTINGINFORMATION).asBool(); + + // KLUDGE: file_resolver is temporarily a pointer. See the + // comment for file_resolver in the header file for more context. + DocParser::file_resolver = &file_resolver; +} + +/*! + Parse the \a source string to build a Text data structure + in \a docPrivate. The Text data structure is a linked list + of Atoms. + + \a metaCommandSet is the set of metacommands that may be + found in \a source. These metacommands are not markup text + commands. They are topic commands and related metacommands. + */ +void DocParser::parse(const QString &source, DocPrivate *docPrivate, + const QSet<QString> &metaCommandSet, const QSet<QString> &possibleTopics) +{ + m_input = source; + m_position = 0; + m_inputLength = m_input.size(); + m_cachedLocation = docPrivate->m_start_loc; + m_cachedPosition = 0; + m_private = docPrivate; + m_private->m_text << Atom::Nop; + m_private->m_topics.clear(); + + m_paragraphState = OutsideParagraph; + m_inTableHeader = false; + m_inTableRow = false; + m_inTableItem = false; + m_indexStartedParagraph = false; + m_pendingParagraphLeftType = Atom::Nop; + m_pendingParagraphRightType = Atom::Nop; + + m_braceDepth = 0; + m_currentSection = Doc::NoSection; + m_openedCommands.push(CMD_OMIT); + m_quoter.reset(); + + CodeMarker *marker = nullptr; + Atom *currentLinkAtom = nullptr; + QString p1, p2; + QStack<bool> preprocessorSkipping; + int numPreprocessorSkipping = 0; + + while (m_position < m_inputLength) { + QChar ch = m_input.at(m_position); + + switch (ch.unicode()) { + case '\\': { + QString cmdStr; + m_backslashPosition = m_position; + ++m_position; + while (m_position < m_inputLength) { + ch = m_input.at(m_position); + if (ch.isLetterOrNumber()) { + cmdStr += ch; + ++m_position; + } else { + break; + } + } + m_endPosition = m_position; + if (cmdStr.isEmpty()) { + if (m_position < m_inputLength) { + enterPara(); + if (m_input.at(m_position).isSpace()) { + skipAllSpaces(); + appendChar(QLatin1Char(' ')); + } else { + appendChar(m_input.at(m_position++)); + } + } + } else { + // Ignore quoting atoms to make appendToCode() + // append to the correct atom. + if (!s_quoting || !isQuote(m_private->m_text.lastAtom())) + m_lastAtom = m_private->m_text.lastAtom(); + + int cmd = s_utilities.cmdHash.value(cmdStr, NOT_A_CMD); + switch (cmd) { + case CMD_A: + enterPara(); + p1 = getArgument(); + appendAtom(Atom(Atom::FormattingLeft, ATOM_FORMATTING_PARAMETER)); + appendAtom(Atom(Atom::String, p1)); + appendAtom(Atom(Atom::FormattingRight, ATOM_FORMATTING_PARAMETER)); + m_private->m_params.insert(p1); + break; + case CMD_BADCODE: + leavePara(); + appendAtom(Atom(Atom::CodeBad, + getCode(CMD_BADCODE, marker, getMetaCommandArgument(cmdStr)))); + break; + case CMD_BR: + enterPara(); + appendAtom(Atom(Atom::BR)); + break; + case CMD_BOLD: + location().warning(QStringLiteral("'\\bold' is deprecated. Use '\\b'")); + Q_FALLTHROUGH(); + case CMD_B: + startFormat(ATOM_FORMATTING_BOLD, cmd); + break; + case CMD_BRIEF: + leavePara(); + enterPara(Atom::BriefLeft, Atom::BriefRight); + break; + case CMD_C: + enterPara(); + p1 = untabifyEtc(getArgument(ArgumentParsingOptions::Verbatim)); + marker = CodeMarker::markerForCode(p1); + appendAtom(Atom(Atom::C, marker->markedUpCode(p1, nullptr, location()))); + break; + case CMD_CAPTION: + leavePara(); + enterPara(Atom::CaptionLeft, Atom::CaptionRight); + break; + case CMD_CODE: + leavePara(); + appendAtom(Atom(Atom::Code, getCode(CMD_CODE, nullptr, getMetaCommandArgument(cmdStr)))); + break; + case CMD_QML: + leavePara(); + appendAtom(Atom(Atom::Qml, + getCode(CMD_QML, CodeMarker::markerForLanguage(QLatin1String("QML")), + getMetaCommandArgument(cmdStr)))); + break; + case CMD_DETAILS: + leavePara(); + appendAtom(Atom(Atom::DetailsLeft, getArgument())); + m_openedCommands.push(cmd); + break; + case CMD_ENDDETAILS: + leavePara(); + appendAtom(Atom(Atom::DetailsRight)); + closeCommand(cmd); + break; + case CMD_DIV: + leavePara(); + p1 = getArgument(ArgumentParsingOptions::Verbatim); + appendAtom(Atom(Atom::DivLeft, p1)); + m_openedCommands.push(cmd); + break; + case CMD_ENDDIV: + leavePara(); + appendAtom(Atom(Atom::DivRight)); + closeCommand(cmd); + break; + case CMD_CODELINE: + if (s_quoting) { + appendAtom(Atom(Atom::CodeQuoteCommand, cmdStr)); + appendAtom(Atom(Atom::CodeQuoteArgument, " ")); + } + if (isCode(m_lastAtom) && m_lastAtom->string().endsWith("\n\n")) + m_lastAtom->chopString(); + appendToCode("\n"); + break; + case CMD_DOTS: { + QString arg = getOptionalArgument(); + if (arg.isEmpty()) + arg = "4"; + if (s_quoting) { + appendAtom(Atom(Atom::CodeQuoteCommand, cmdStr)); + appendAtom(Atom(Atom::CodeQuoteArgument, arg)); + } + if (isCode(m_lastAtom) && m_lastAtom->string().endsWith("\n\n")) + m_lastAtom->chopString(); + + int indent = arg.toInt(); + for (int i = 0; i < indent; ++i) + appendToCode(" "); + appendToCode("...\n"); + break; + } + case CMD_ELSE: + if (!preprocessorSkipping.empty()) { + if (preprocessorSkipping.top()) { + --numPreprocessorSkipping; + } else { + ++numPreprocessorSkipping; + } + preprocessorSkipping.top() = !preprocessorSkipping.top(); + (void)getRestOfLine(); // ### should ensure that it's empty + if (numPreprocessorSkipping) + skipToNextPreprocessorCommand(); + } else { + location().warning( + QStringLiteral("Unexpected '\\%1'").arg(cmdName(CMD_ELSE))); + } + break; + case CMD_ENDCODE: + closeCommand(cmd); + break; + case CMD_ENDQML: + closeCommand(cmd); + break; + case CMD_ENDFOOTNOTE: + if (closeCommand(cmd)) { + leavePara(); + appendAtom(Atom(Atom::FootnoteRight)); + } + break; + case CMD_ENDIF: + if (preprocessorSkipping.size() > 0) { + if (preprocessorSkipping.pop()) + --numPreprocessorSkipping; + (void)getRestOfLine(); // ### should ensure that it's empty + if (numPreprocessorSkipping) + skipToNextPreprocessorCommand(); + } else { + location().warning( + QStringLiteral("Unexpected '\\%1'").arg(cmdName(CMD_ENDIF))); + } + break; + case CMD_ENDLEGALESE: + if (closeCommand(cmd)) { + leavePara(); + appendAtom(Atom(Atom::LegaleseRight)); + } + break; + case CMD_ENDLINK: + if (closeCommand(cmd)) { + if (m_private->m_text.lastAtom()->type() == Atom::String + && m_private->m_text.lastAtom()->string().endsWith(QLatin1Char(' '))) + m_private->m_text.lastAtom()->chopString(); + appendAtom(Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK)); + } + break; + case CMD_ENDLIST: + if (closeCommand(cmd)) { + leavePara(); + if (m_openedLists.top().isStarted()) { + appendAtom(Atom(Atom::ListItemRight, m_openedLists.top().styleString())); + appendAtom(Atom(Atom::ListRight, m_openedLists.top().styleString())); + } + m_openedLists.pop(); + } + break; + case CMD_ENDOMIT: + closeCommand(cmd); + break; + case CMD_ENDQUOTATION: + if (closeCommand(cmd)) { + leavePara(); + appendAtom(Atom(Atom::QuotationRight)); + } + break; + case CMD_ENDRAW: + location().warning( + QStringLiteral("Unexpected '\\%1'").arg(cmdName(CMD_ENDRAW))); + break; + case CMD_ENDSECTION1: + endSection(Doc::Section1, cmd); + break; + case CMD_ENDSECTION2: + endSection(Doc::Section2, cmd); + break; + case CMD_ENDSECTION3: + endSection(Doc::Section3, cmd); + break; + case CMD_ENDSECTION4: + endSection(Doc::Section4, cmd); + break; + case CMD_ENDSIDEBAR: + if (closeCommand(cmd)) { + leavePara(); + appendAtom(Atom(Atom::SidebarRight)); + } + break; + case CMD_ENDTABLE: + if (closeCommand(cmd)) { + leaveTableRow(); + appendAtom(Atom(Atom::TableRight)); + } + break; + case CMD_FOOTNOTE: + if (openCommand(cmd)) { + enterPara(); + appendAtom(Atom(Atom::FootnoteLeft)); + } + break; + case CMD_ANNOTATEDLIST: { + // Optional sorting directive [ascending|descending] + if (isLeftBracketAhead()) + p2 = getBracketedArgument(); + else + p2.clear(); + appendAtom(Atom(Atom::AnnotatedList, getArgument(), p2)); + } break; + case CMD_SINCELIST: + leavePara(); + appendAtom(Atom(Atom::SinceList, getRestOfLine().simplified())); + break; + case CMD_GENERATELIST: { + // Optional sorting directive [ascending|descending] + if (isLeftBracketAhead()) + p2 = getBracketedArgument(); + else + p2.clear(); + QString arg1 = getArgument(); + QString arg2 = getOptionalArgument(); + if (!arg2.isEmpty()) + arg1 += " " + arg2; + appendAtom(Atom(Atom::GeneratedList, arg1, p2)); + } break; + case CMD_HEADER: + if (m_openedCommands.top() == CMD_TABLE) { + leaveTableRow(); + appendAtom(Atom(Atom::TableHeaderLeft)); + m_inTableHeader = true; + } else { + if (m_openedCommands.contains(CMD_TABLE)) + location().warning(QStringLiteral("Cannot use '\\%1' within '\\%2'") + .arg(cmdName(CMD_HEADER), + cmdName(m_openedCommands.top()))); + else + location().warning( + QStringLiteral("Cannot use '\\%1' outside of '\\%2'") + .arg(cmdName(CMD_HEADER), cmdName(CMD_TABLE))); + } + break; + case CMD_I: + location().warning(QStringLiteral( + "'\\i' is deprecated. Use '\\e' for italic or '\\li' for list item")); + Q_FALLTHROUGH(); + case CMD_E: + startFormat(ATOM_FORMATTING_ITALIC, cmd); + break; + case CMD_HR: + leavePara(); + appendAtom(Atom(Atom::HR)); + break; + case CMD_IF: + preprocessorSkipping.push(!Tokenizer::isTrue(getRestOfLine())); + if (preprocessorSkipping.top()) + ++numPreprocessorSkipping; + if (numPreprocessorSkipping) + skipToNextPreprocessorCommand(); + break; + case CMD_IMAGE: + leaveValueList(); + appendAtom(Atom(Atom::Image, getArgument())); + appendAtom(Atom(Atom::ImageText, getRestOfLine())); + break; + case CMD_IMPORTANT: + leavePara(); + enterPara(Atom::ImportantLeft, Atom::ImportantRight); + break; + case CMD_INCLUDE: + case CMD_INPUT: { + QString fileName = getArgument(); + QStringList parameters; + QString identifier; + if (isLeftBraceAhead()) { + identifier = getArgument(); + while (isLeftBraceAhead() && parameters.size() < 9) + parameters << getArgument(); + } else { + identifier = getRestOfLine(); + } + include(fileName, identifier, parameters); + break; + } + case CMD_INLINEIMAGE: + enterPara(); + appendAtom(Atom(Atom::InlineImage, getArgument())); + //Append ImageText only if the following + //argument is enclosed in braces. + if (isLeftBraceAhead()) { + appendAtom(Atom(Atom::ImageText, getArgument())); + appendAtom(Atom(Atom::String, " ")); + } + break; + case CMD_INDEX: + if (m_paragraphState == OutsideParagraph) { + enterPara(); + m_indexStartedParagraph = true; + } else { + const Atom *last = m_private->m_text.lastAtom(); + if (m_indexStartedParagraph + && (last->type() != Atom::FormattingRight + || last->string() != ATOM_FORMATTING_INDEX)) + m_indexStartedParagraph = false; + } + startFormat(ATOM_FORMATTING_INDEX, cmd); + break; + case CMD_KEYWORD: + leavePara(); + insertKeyword(getRestOfLine()); + break; + case CMD_L: + enterPara(); + if (isLeftBracketAhead()) + p2 = getBracketedArgument(); + + p1 = getArgument(); + + appendAtom(LinkAtom(p1, p2, location())); + + if (isLeftBraceAhead()) { + currentLinkAtom = m_private->m_text.lastAtom(); + startFormat(ATOM_FORMATTING_LINK, cmd); + } else { + appendAtom(Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)); + appendAtom(Atom(Atom::String, cleanLink(p1))); + appendAtom(Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK)); + } + + p2.clear(); + + break; + case CMD_LEGALESE: + leavePara(); + if (openCommand(cmd)) + appendAtom(Atom(Atom::LegaleseLeft)); + docPrivate->m_hasLegalese = true; + break; + case CMD_LINK: + if (openCommand(cmd)) { + enterPara(); + p1 = getArgument(); + appendAtom(Atom(Atom::Link, p1)); + appendAtom(Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)); + skipSpacesOrOneEndl(); + } + break; + case CMD_LIST: + if (openCommand(cmd)) { + leavePara(); + m_openedLists.push(OpenedList(location(), getOptionalArgument())); + } + break; + case CMD_META: + m_private->constructExtra(); + p1 = getArgument(); + m_private->extra->m_metaMap.insert(p1, getArgument()); + break; + case CMD_NOTE: + leavePara(); + enterPara(Atom::NoteLeft, Atom::NoteRight); + break; + case CMD_O: + location().warning(QStringLiteral("'\\o' is deprecated. Use '\\li'")); + Q_FALLTHROUGH(); + case CMD_LI: + leavePara(); + if (m_openedCommands.top() == CMD_LIST) { + if (m_openedLists.top().isStarted()) + appendAtom(Atom(Atom::ListItemRight, m_openedLists.top().styleString())); + else + appendAtom(Atom(Atom::ListLeft, m_openedLists.top().styleString())); + m_openedLists.top().next(); + appendAtom(Atom(Atom::ListItemNumber, m_openedLists.top().numberString())); + appendAtom(Atom(Atom::ListItemLeft, m_openedLists.top().styleString())); + enterPara(); + } else if (m_openedCommands.top() == CMD_TABLE) { + p1 = "1,1"; + p2.clear(); + if (isLeftBraceAhead()) { + p1 = getArgument(); + if (isLeftBraceAhead()) + p2 = getArgument(); + } + + if (!m_inTableHeader && !m_inTableRow) { + location().warning( + QStringLiteral("Missing '\\%1' or '\\%2' before '\\%3'") + .arg(cmdName(CMD_HEADER), cmdName(CMD_ROW), + cmdName(CMD_LI))); + appendAtom(Atom(Atom::TableRowLeft)); + m_inTableRow = true; + } else if (m_inTableItem) { + appendAtom(Atom(Atom::TableItemRight)); + m_inTableItem = false; + } + + appendAtom(Atom(Atom::TableItemLeft, p1, p2)); + m_inTableItem = true; + } else + location().warning( + QStringLiteral("Command '\\%1' outside of '\\%2' and '\\%3'") + .arg(cmdName(cmd), cmdName(CMD_LIST), cmdName(CMD_TABLE))); + break; + case CMD_OMIT: + getUntilEnd(cmd); + break; + case CMD_OMITVALUE: { + leavePara(); + p1 = getArgument(); + if (!m_private->m_enumItemList.contains(p1)) + m_private->m_enumItemList.append(p1); + if (!m_private->m_omitEnumItemList.contains(p1)) + m_private->m_omitEnumItemList.append(p1); + skipSpacesOrOneEndl(); + // Skip potential description paragraph + while (m_position < m_inputLength && !isBlankLine()) { + skipAllSpaces(); + if (qsizetype pos = m_position; pos < m_input.size() + && m_input.at(pos++).unicode() == '\\') { + QString nextCmdStr; + while (pos < m_input.size() && m_input[pos].isLetterOrNumber()) + nextCmdStr += m_input[pos++]; + int nextCmd = s_utilities.cmdHash.value(cmdStr, NOT_A_CMD); + if (nextCmd == cmd || nextCmd == CMD_VALUE) + break; + } + getRestOfLine(); + } + break; + } + case CMD_COMPARESWITH: + leavePara(); + p1 = getRestOfLine(); + if (openCommand(cmd)) + appendAtom(Atom(Atom::ComparesLeft, p1)); + break; + case CMD_ENDCOMPARESWITH: + if (closeCommand(cmd)) { + leavePara(); + appendAtom(Atom(Atom::ComparesRight)); + processComparesWithCommand(m_private, location()); + } + break; + case CMD_PRINTLINE: { + leavePara(); + QString rest = getRestOfLine(); + if (s_quoting) { + appendAtom(Atom(Atom::CodeQuoteCommand, cmdStr)); + appendAtom(Atom(Atom::CodeQuoteArgument, rest)); + } + appendToCode(m_quoter.quoteLine(location(), cmdStr, rest)); + break; + } + case CMD_PRINTTO: { + leavePara(); + QString rest = getRestOfLine(); + if (s_quoting) { + appendAtom(Atom(Atom::CodeQuoteCommand, cmdStr)); + appendAtom(Atom(Atom::CodeQuoteArgument, rest)); + } + appendToCode(m_quoter.quoteTo(location(), cmdStr, rest)); + break; + } + case CMD_PRINTUNTIL: { + leavePara(); + QString rest = getRestOfLine(); + if (s_quoting) { + appendAtom(Atom(Atom::CodeQuoteCommand, cmdStr)); + appendAtom(Atom(Atom::CodeQuoteArgument, rest)); + } + appendToCode(m_quoter.quoteUntil(location(), cmdStr, rest)); + break; + } + case CMD_QUOTATION: + if (openCommand(cmd)) { + leavePara(); + appendAtom(Atom(Atom::QuotationLeft)); + } + break; + case CMD_QUOTEFILE: { + leavePara(); + + QString fileName = getArgument(); + quoteFromFile(fileName); + if (s_quoting) { + appendAtom(Atom(Atom::CodeQuoteCommand, cmdStr)); + appendAtom(Atom(Atom::CodeQuoteArgument, fileName)); + } + appendAtom(Atom(Atom::Code, m_quoter.quoteTo(location(), cmdStr, QString()))); + m_quoter.reset(); + break; + } + case CMD_QUOTEFROMFILE: { + leavePara(); + QString arg = getArgument(); + if (s_quoting) { + appendAtom(Atom(Atom::CodeQuoteCommand, cmdStr)); + appendAtom(Atom(Atom::CodeQuoteArgument, arg)); + } + quoteFromFile(arg); + break; + } + case CMD_RAW: + leavePara(); + p1 = getRestOfLine(); + if (p1.isEmpty()) + location().warning(QStringLiteral("Missing format name after '\\%1'") + .arg(cmdName(CMD_RAW))); + appendAtom(Atom(Atom::FormatIf, p1)); + appendAtom(Atom(Atom::RawString, untabifyEtc(getUntilEnd(cmd)))); + appendAtom(Atom(Atom::FormatElse)); + appendAtom(Atom(Atom::FormatEndif)); + break; + case CMD_ROW: + if (m_openedCommands.top() == CMD_TABLE) { + p1.clear(); + if (isLeftBraceAhead()) + p1 = getArgument(ArgumentParsingOptions::Verbatim); + leaveTableRow(); + appendAtom(Atom(Atom::TableRowLeft, p1)); + m_inTableRow = true; + } else { + if (m_openedCommands.contains(CMD_TABLE)) + location().warning(QStringLiteral("Cannot use '\\%1' within '\\%2'") + .arg(cmdName(CMD_ROW), + cmdName(m_openedCommands.top()))); + else + location().warning(QStringLiteral("Cannot use '\\%1' outside of '\\%2'") + .arg(cmdName(CMD_ROW), cmdName(CMD_TABLE))); + } + break; + case CMD_SA: + parseAlso(); + break; + case CMD_SECTION1: + startSection(Doc::Section1, cmd); + break; + case CMD_SECTION2: + startSection(Doc::Section2, cmd); + break; + case CMD_SECTION3: + startSection(Doc::Section3, cmd); + break; + case CMD_SECTION4: + startSection(Doc::Section4, cmd); + break; + case CMD_SIDEBAR: + if (openCommand(cmd)) { + leavePara(); + appendAtom(Atom(Atom::SidebarLeft)); + } + break; + case CMD_SKIPLINE: { + leavePara(); + QString rest = getRestOfLine(); + if (s_quoting) { + appendAtom(Atom(Atom::CodeQuoteCommand, cmdStr)); + appendAtom(Atom(Atom::CodeQuoteArgument, rest)); + } + m_quoter.quoteLine(location(), cmdStr, rest); + break; + } + case CMD_SKIPTO: { + leavePara(); + QString rest = getRestOfLine(); + if (s_quoting) { + appendAtom(Atom(Atom::CodeQuoteCommand, cmdStr)); + appendAtom(Atom(Atom::CodeQuoteArgument, rest)); + } + m_quoter.quoteTo(location(), cmdStr, rest); + break; + } + case CMD_SKIPUNTIL: { + leavePara(); + QString rest = getRestOfLine(); + if (s_quoting) { + appendAtom(Atom(Atom::CodeQuoteCommand, cmdStr)); + appendAtom(Atom(Atom::CodeQuoteArgument, rest)); + } + m_quoter.quoteUntil(location(), cmdStr, rest); + break; + } + case CMD_SPAN: + p1 = ATOM_FORMATTING_SPAN + getArgument(ArgumentParsingOptions::Verbatim); + startFormat(p1, cmd); + break; + case CMD_SNIPPET: { + leavePara(); + QString snippet = getArgument(); + QString identifier = getRestOfLine(); + if (s_quoting) { + appendAtom(Atom(Atom::SnippetCommand, cmdStr)); + appendAtom(Atom(Atom::SnippetLocation, snippet)); + appendAtom(Atom(Atom::SnippetIdentifier, identifier)); + } + marker = CodeMarker::markerForFileName(snippet); + quoteFromFile(snippet); + appendToCode(m_quoter.quoteSnippet(location(), identifier), marker->atomType()); + break; + } + case CMD_SUB: + startFormat(ATOM_FORMATTING_SUBSCRIPT, cmd); + break; + case CMD_SUP: + startFormat(ATOM_FORMATTING_SUPERSCRIPT, cmd); + break; + case CMD_TABLE: + leaveValueList(); + p1 = getOptionalArgument(); + p2 = getOptionalArgument(); + if (openCommand(cmd)) { + leavePara(); + appendAtom(Atom(Atom::TableLeft, p1, p2)); + m_inTableHeader = false; + m_inTableRow = false; + m_inTableItem = false; + } + break; + case CMD_TABLEOFCONTENTS: + p1 = "1"; + if (isLeftBraceAhead()) + p1 = getArgument(); + p1 += QLatin1Char(','); + p1 += QString::number((int)getSectioningUnit()); + appendAtom(Atom(Atom::TableOfContents, p1)); + break; + case CMD_TARGET: + if (m_openedCommands.top() == CMD_TABLE && !m_inTableItem) { + location().warning("Found a \\target command outside table item in a table.\n" + "Move the \\target inside the \\li to resolve this warning."); + } + insertTarget(getRestOfLine()); + break; + case CMD_TM: + // Ignore command while parsing \section<N> argument + if (m_paragraphState != InSingleLineParagraph) + startFormat(ATOM_FORMATTING_TRADEMARK, cmd); + break; + case CMD_TT: + startFormat(ATOM_FORMATTING_TELETYPE, cmd); + break; + case CMD_UICONTROL: + startFormat(ATOM_FORMATTING_UICONTROL, cmd); + break; + case CMD_UNDERLINE: + startFormat(ATOM_FORMATTING_UNDERLINE, cmd); + break; + case CMD_UNICODE: { + enterPara(); + p1 = getArgument(); + bool ok; + uint unicodeChar = p1.toUInt(&ok, 0); + if (!ok || (unicodeChar == 0x0000) || (unicodeChar > 0xFFFE)) + location().warning( + QStringLiteral("Invalid Unicode character '%1' specified with '%2'") + .arg(p1, cmdName(CMD_UNICODE))); + else + appendAtom(Atom(Atom::String, QChar(unicodeChar))); + break; + } + case CMD_VALUE: + leaveValue(); + if (m_openedLists.top().style() == OpenedList::Value) { + QString p2; + p1 = getArgument(); + if (p1.startsWith(QLatin1String("[since ")) + && p1.endsWith(QLatin1String("]"))) { + p2 = p1.mid(7, p1.size() - 8); + p1 = getArgument(); + } + if (!m_private->m_enumItemList.contains(p1)) + m_private->m_enumItemList.append(p1); + + m_openedLists.top().next(); + appendAtom(Atom(Atom::ListTagLeft, ATOM_LIST_VALUE)); + appendAtom(Atom(Atom::String, p1)); + appendAtom(Atom(Atom::ListTagRight, ATOM_LIST_VALUE)); + if (!p2.isEmpty()) { + appendAtom(Atom(Atom::SinceTagLeft, ATOM_LIST_VALUE)); + appendAtom(Atom(Atom::String, p2)); + appendAtom(Atom(Atom::SinceTagRight, ATOM_LIST_VALUE)); + } + appendAtom(Atom(Atom::ListItemLeft, ATOM_LIST_VALUE)); + + skipSpacesOrOneEndl(); + if (isBlankLine()) + appendAtom(Atom(Atom::Nop)); + } else { + // ### unknown problems + } + break; + case CMD_WARNING: + leavePara(); + enterPara(Atom::WarningLeft, Atom::WarningRight); + break; + case CMD_OVERLOAD: + leavePara(); + m_private->m_metacommandsUsed.insert(cmdStr); + p1.clear(); + if (!isBlankLine()) + p1 = getRestOfLine(); + if (!p1.isEmpty()) { + appendAtom(Atom(Atom::ParaLeft)); + appendAtom(Atom(Atom::String, "This function overloads ")); + appendAtom(Atom(Atom::AutoLink, p1)); + appendAtom(Atom(Atom::String, ".")); + appendAtom(Atom(Atom::ParaRight)); + } else { + appendAtom(Atom(Atom::ParaLeft)); + appendAtom(Atom(Atom::String, "This is an overloaded function.")); + appendAtom(Atom(Atom::ParaRight)); + p1 = getMetaCommandArgument(cmdStr); + } + m_private->m_metaCommandMap[cmdStr].append(ArgPair(p1, QString())); + break; + case NOT_A_CMD: + if (metaCommandSet.contains(cmdStr)) { + QString arg; + QString bracketedArg; + m_private->m_metacommandsUsed.insert(cmdStr); + if (isLeftBracketAhead()) + bracketedArg = getBracketedArgument(); + // Force a linebreak after \obsolete or \deprecated + // to treat potential arguments as a new text paragraph. + if (m_position < m_inputLength + && (cmdStr == QLatin1String("obsolete") + || cmdStr == QLatin1String("deprecated"))) + m_input[m_position] = '\n'; + else + arg = getMetaCommandArgument(cmdStr); + m_private->m_metaCommandMap[cmdStr].append(ArgPair(arg, bracketedArg)); + if (possibleTopics.contains(cmdStr)) { + if (!cmdStr.endsWith(QLatin1String("propertygroup"))) + m_private->m_topics.append(Topic(cmdStr, arg)); + } + } else if (s_utilities.macroHash.contains(cmdStr)) { + const Macro ¯o = s_utilities.macroHash.value(cmdStr); + QStringList macroArgs; + int numPendingFi = 0; + int numFormatDefs = 0; + for (auto it = macro.m_otherDefs.constBegin(); + it != macro.m_otherDefs.constEnd(); ++it) { + if (it.key() != "match") { + if (numFormatDefs == 0) + macroArgs = getMacroArguments(cmdStr, macro); + appendAtom(Atom(Atom::FormatIf, it.key())); + expandMacro(*it, macroArgs); + ++numFormatDefs; + if (it == macro.m_otherDefs.constEnd()) { + appendAtom(Atom(Atom::FormatEndif)); + } else { + appendAtom(Atom(Atom::FormatElse)); + ++numPendingFi; + } + } + } + while (numPendingFi-- > 0) + appendAtom(Atom(Atom::FormatEndif)); + + if (!macro.m_defaultDef.isEmpty()) { + if (numFormatDefs > 0) { + macro.m_defaultDefLocation.warning( + QStringLiteral("Macro cannot have both " + "format-specific and qdoc-" + "syntax definitions")); + } else { + QString expanded = expandMacroToString(cmdStr, macro); + m_input.replace(m_backslashPosition, + m_endPosition - m_backslashPosition, expanded); + m_inputLength = m_input.size(); + m_position = m_backslashPosition; + } + } + } else if (isAutoLinkString(cmdStr)) { + appendWord(cmdStr); + } else { + if (!cmdStr.endsWith("propertygroup")) { + // The QML property group commands are no longer required + // for grouping QML properties. They are allowed but ignored. + location().warning(QStringLiteral("Unknown command '\\%1'").arg(cmdStr), + detailsUnknownCommand(metaCommandSet, cmdStr)); + } + enterPara(); + appendAtom(Atom(Atom::UnknownCommand, cmdStr)); + } + } + } // case '\\' (qdoc markup command) + break; + } + case '-': { // Catch en-dash (--) and em-dash (---) markup here. + enterPara(); + qsizetype dashCount = 1; + ++m_position; + + // Figure out how many hyphens in a row. + while ((m_position < m_inputLength) && (m_input.at(m_position) == '-')) { + ++dashCount; + ++m_position; + } + + if (dashCount == 3) { + // 3 hyphens, append an em-dash character. + const QChar emDash(8212); + appendChar(emDash); + } else if (dashCount == 2) { + // 2 hyphens; append an en-dash character. + const QChar enDash(8211); + appendChar(enDash); + } else { + // dashCount is either one or more than three. Append a hyphen + // the appropriate number of times. This ensures '----' doesn't + // end up as an em-dash followed by a hyphen in the output. + for (qsizetype i = 0; i < dashCount; ++i) + appendChar('-'); + } + break; + } + case '{': + enterPara(); + appendChar('{'); + ++m_braceDepth; + ++m_position; + break; + case '}': { + --m_braceDepth; + ++m_position; + + auto format = m_pendingFormats.find(m_braceDepth); + if (format == m_pendingFormats.end()) { + enterPara(); + appendChar('}'); + } else { + const auto &last{m_private->m_text.lastAtom()->string()}; + appendAtom(Atom(Atom::FormattingRight, *format)); + if (*format == ATOM_FORMATTING_INDEX) { + if (m_indexStartedParagraph) + skipAllSpaces(); + } else if (*format == ATOM_FORMATTING_LINK) { + // hack for C++ to support links like + // \l{QString::}{count()} + if (currentLinkAtom && currentLinkAtom->string().endsWith("::")) { + QString suffix = + Text::subText(currentLinkAtom, m_private->m_text.lastAtom()) + .toString(); + currentLinkAtom->concatenateString(suffix); + } + currentLinkAtom = nullptr; + } else if (*format == ATOM_FORMATTING_TRADEMARK) { + m_private->m_text.lastAtom()->append(last); + } + m_pendingFormats.erase(format); + } + break; + } + // Do not parse content after '//!' comments + case '/': { + if (m_position + 2 < m_inputLength) + if (m_input.at(m_position + 1) == '/') + if (m_input.at(m_position + 2) == '!') { + m_position += 2; + getRestOfLine(); + if (m_input.at(m_position - 1) == '\n') + --m_position; + break; + } + Q_FALLTHROUGH(); // fall through + } + default: { + bool newWord; + switch (m_private->m_text.lastAtom()->type()) { + case Atom::ParaLeft: + newWord = true; + break; + default: + newWord = false; + } + + if (m_paragraphState == OutsideParagraph) { + if (ch.isSpace()) { + ++m_position; + newWord = false; + } else { + enterPara(); + newWord = true; + } + } else { + if (ch.isSpace()) { + ++m_position; + if ((ch == '\n') + && (m_paragraphState == InSingleLineParagraph || isBlankLine())) { + leavePara(); + newWord = false; + } else { + appendChar(' '); + newWord = true; + } + } else { + newWord = true; + } + } + + if (newWord) { + qsizetype startPos = m_position; + // No auto-linking inside links + bool autolink = (!m_pendingFormats.isEmpty() && + m_pendingFormats.last() == ATOM_FORMATTING_LINK) ? + false : isAutoLinkString(m_input, m_position); + if (m_position == startPos) { + if (!ch.isSpace()) { + appendChar(ch); + ++m_position; + } + } else { + QString word = m_input.mid(startPos, m_position - startPos); + if (autolink) { + if (s_ignoreWords.contains(word) || word.startsWith(QString("__"))) + appendWord(word); + else + appendAtom(Atom(Atom::AutoLink, word)); + } else { + appendWord(word); + } + } + } + } // default: + } // switch (ch.unicode()) + } + leaveValueList(); + + // for compatibility + if (m_openedCommands.top() == CMD_LEGALESE) { + appendAtom(Atom(Atom::LegaleseRight)); + m_openedCommands.pop(); + } + + if (m_openedCommands.top() != CMD_OMIT) { + location().warning( + QStringLiteral("Missing '\\%1'").arg(endCmdName(m_openedCommands.top()))); + } else if (preprocessorSkipping.size() > 0) { + location().warning(QStringLiteral("Missing '\\%1'").arg(cmdName(CMD_ENDIF))); + } + + if (m_currentSection > Doc::NoSection) { + appendAtom(Atom(Atom::SectionRight, QString::number(m_currentSection))); + m_currentSection = Doc::NoSection; + } + + m_private->m_text.stripFirstAtom(); +} + +/*! + Returns the current location. + */ +Location &DocParser::location() +{ + while (!m_openedInputs.isEmpty() && m_openedInputs.top() <= m_position) { + m_cachedLocation.pop(); + m_cachedPosition = m_openedInputs.pop(); + } + while (m_cachedPosition < m_position) + m_cachedLocation.advance(m_input.at(m_cachedPosition++)); + return m_cachedLocation; +} + +QString DocParser::detailsUnknownCommand(const QSet<QString> &metaCommandSet, const QString &str) +{ + QSet<QString> commandSet = metaCommandSet; + int i = 0; + while (cmds[i].name != nullptr) { + commandSet.insert(cmds[i].name); + ++i; + } + + QString best = nearestName(str, commandSet); + if (best.isEmpty()) + return QString(); + return QStringLiteral("Maybe you meant '\\%1'?").arg(best); +} + +/*! + \internal + + Issues a warning about the duplicate definition of a target or keyword in + at \a location. \a duplicateDefinition is the target being processed; the + already registered definition is \a previousDefinition. + */ +static void warnAboutPreexistingTarget(const Location &location, const QString &duplicateDefinition, const QString &previousDefinition) +{ + location.warning( + QStringLiteral("Duplicate target name '%1'. The previous occurrence is here: %2") + .arg(duplicateDefinition, previousDefinition)); +} + +/*! + \internal + + \brief Registers \a target as a linkable entity. + + The main purpose of this method is to register a target as defined + along with its location, so that becomes a valid link target from other + parts of the documentation. + + If the \a target name is already registered, a warning is issued, + as multiple definitions are problematic. + + \sa insertKeyword, target-command + */ +void DocParser::insertTarget(const QString &target) +{ + if (m_targetMap.contains(target)) + return warnAboutPreexistingTarget(location(), target, m_targetMap[target].toString()); + + m_targetMap.insert(target, location()); + m_private->constructExtra(); + + appendAtom(Atom(Atom::Target, target)); + m_private->extra->m_targets.append(m_private->m_text.lastAtom()); +} + +/*! + \internal + + \brief Registers \a keyword as a linkable entity. + + The main purpose of this method is to register a keyword as defined + along with its location, so that becomes a valid link target from other + parts of the documentation. + + If the \a keyword name is already registered, a warning is issued, + as multiple definitions are problematic. + + \sa insertTarget, keyword-command + */ +void DocParser::insertKeyword(const QString &keyword) +{ + if (m_targetMap.contains(keyword)) + return warnAboutPreexistingTarget(location(), keyword, m_targetMap[keyword].toString()); + + m_targetMap.insert(keyword, location()); + m_private->constructExtra(); + + appendAtom(Atom(Atom::Keyword, keyword)); + m_private->extra->m_keywords.append(m_private->m_text.lastAtom()); +} + +void DocParser::include(const QString &fileName, const QString &identifier, const QStringList ¶meters) +{ + if (location().depth() > 16) + location().fatal(QStringLiteral("Too many nested '\\%1's").arg(cmdName(CMD_INCLUDE))); + QString filePath = Config::instance().getIncludeFilePath(fileName); + if (filePath.isEmpty()) { + location().warning(QStringLiteral("Cannot find qdoc include file '%1'").arg(fileName)); + } else { + QFile inFile(filePath); + if (!inFile.open(QFile::ReadOnly)) { + location().warning( + QStringLiteral("Cannot open qdoc include file '%1'").arg(filePath)); + } else { + location().push(fileName); + QTextStream inStream(&inFile); + QString includedContent = inStream.readAll(); + inFile.close(); + + if (identifier.isEmpty()) { + expandArgumentsInString(includedContent, parameters); + m_input.insert(m_position, includedContent); + m_inputLength = m_input.size(); + m_openedInputs.push(m_position + includedContent.size()); + } else { + QStringList lineBuffer = includedContent.split(QLatin1Char('\n')); + qsizetype bufLen{lineBuffer.size()}; + qsizetype i; + QStringView trimmedLine; + for (i = 0; i < bufLen; ++i) { + trimmedLine = QStringView{lineBuffer[i]}.trimmed(); + if (trimmedLine.startsWith(QLatin1String("//!")) && + trimmedLine.contains(identifier)) + break; + } + if (i < bufLen - 1) { + ++i; + } else { + location().warning( + QStringLiteral("Cannot find '%1' in '%2'").arg(identifier, filePath)); + return; + } + QString result; + do { + trimmedLine = QStringView{lineBuffer[i]}.trimmed(); + if (trimmedLine.startsWith(QLatin1String("//!")) && + trimmedLine.contains(identifier)) + break; + else + result += lineBuffer[i] + QLatin1Char('\n'); + ++i; + } while (i < bufLen); + + expandArgumentsInString(result, parameters); + if (result.isEmpty()) { + location().warning(QStringLiteral("Empty qdoc snippet '%1' in '%2'") + .arg(identifier, filePath)); + } else { + m_input.insert(m_position, result); + m_inputLength = m_input.size(); + m_openedInputs.push(m_position + result.size()); + } + } + } + } +} + +void DocParser::startFormat(const QString &format, int cmd) +{ + enterPara(); + + for (const auto &item : std::as_const(m_pendingFormats)) { + if (item == format) { + location().warning(QStringLiteral("Cannot nest '\\%1' commands").arg(cmdName(cmd))); + return; + } + } + + appendAtom(Atom(Atom::FormattingLeft, format)); + + if (isLeftBraceAhead()) { + skipSpacesOrOneEndl(); + m_pendingFormats.insert(m_braceDepth, format); + ++m_braceDepth; + ++m_position; + } else { + const auto &arg{getArgument()}; + appendAtom(Atom(Atom::String, arg)); + appendAtom(Atom(Atom::FormattingRight, format)); + if (format == ATOM_FORMATTING_INDEX && m_indexStartedParagraph) { + skipAllSpaces(); + m_indexStartedParagraph = false; + } else if (format == ATOM_FORMATTING_TRADEMARK) { + m_private->m_text.lastAtom()->append(arg); + } + } +} + +bool DocParser::openCommand(int cmd) +{ + int outer = m_openedCommands.top(); + bool ok = true; + + if (cmd == CMD_COMPARESWITH && m_openedCommands.contains(cmd)) { + location().warning(u"Cannot nest '\\%1' commands"_s.arg(cmdName(cmd))); + return false; + } else if (cmd != CMD_LINK) { + if (outer == CMD_LIST) { + ok = (cmd == CMD_FOOTNOTE || cmd == CMD_LIST); + } else if (outer == CMD_SIDEBAR) { + ok = (cmd == CMD_LIST || cmd == CMD_QUOTATION || cmd == CMD_SIDEBAR); + } else if (outer == CMD_QUOTATION) { + ok = (cmd == CMD_LIST); + } else if (outer == CMD_TABLE) { + ok = (cmd == CMD_LIST || cmd == CMD_FOOTNOTE || cmd == CMD_QUOTATION); + } else if (outer == CMD_FOOTNOTE || outer == CMD_LINK) { + ok = false; + } + } + + if (ok) { + m_openedCommands.push(cmd); + } else { + location().warning( + QStringLiteral("Can't use '\\%1' in '\\%2'").arg(cmdName(cmd), cmdName(outer))); + } + return ok; +} + +/*! + Returns \c true if \a word qualifies for auto-linking. + + A word qualifies for auto-linking if either: + + \list + \li It is composed of only upper and lowercase characters + \li AND It contains at least one uppercase character that is not + the first character of word + \li AND it contains at least two lowercase characters + \endlist + + Or + + \list + \li It is composed only of uppercase characters, lowercase + characters, characters in [_@] and the \c {"::"} sequence. + \li It contains at least one uppercase character that is not + the first character of word or it contains at least one + lowercase character + \li AND it contains at least one character in [_@] or it + contains at least one \c {"::"} sequence. + \endlist + + Inserting or suffixing, but not prefixing, any sequence in [0-9]+ + in a word that qualifies for auto-linking by the above rules + preserves the auto-linkability of the word. + + Suffixing the sequence \c {"()"} to a word that qualifies for + auto-linking by the above rules preserves the auto-linkability of + a word. + + FInally, a word qualifies for auto-linking if: + + \list + \li It is composed of only uppercase characters, lowercase + characters and the sequence \c {"()"} + \li AND it contains one lowercase character and a sequence of zero, one + or two upper or lowercase characters + \li AND it contains exactly one sequence \c {"()"} + \li AND it contains one sequence \c {"()"} as the last two + characters of word + \endlist + + For example, \c {"fOo"}, \c {"FooBar"} and \c {"foobaR"} qualify + for auto-linking by the first rule. + + \c {"QT_DEBUG"}, \c {"::Qt"} and \c {"std::move"} qualifies for + auto-linking by the second rule. + + \c {"SIMDVector256"} qualifies by suffixing \c {"SIMDVector"}, + which qualifies by the first rule, with the sequence \c {"256"} + + \c {"FooBar::Bar()"} qualifies by suffixing \c {"FooBar::Bar"}, + which qualifies by the first and second rule, with the sequence \c + {"()"}. + + \c {"Foo()"} and \c {"a()"} qualifies by the last rule. + + Instead, \c {"Q"}, \c {"flower"}, \c {"_"} and \c {"()"} do not + qualify for auto-linking. + + The rules are intended as a heuristic to catch common cases in the + Qt documentation where a word might represent an important + documented element such as a class or a method that could be + linked to while at the same time avoiding catching common words + such as \c {"A"} or \c {"Nonetheless"}. + + The heuristic assumes that Qt's codebase respects a style where + camelCasing is the standard for most of the elements, a function + call is identified by the use of parenthesis and certain elements, + such as macros, might be fully uppercase. + + Furthemore, it assumes that the Qt codebase is written in a + language that has an identifier grammar similar to the one for + C++. +*/ +inline bool DocParser::isAutoLinkString(const QString &word) +{ + qsizetype start = 0; + return isAutoLinkString(word, start) && (start == word.size()); +} + +/*! + Returns \c true if a prefix of a substring of \a word qualifies + for auto-linking. + + Respects the same parsing rules as the unary overload. + + \a curPos defines the offset, from the first character of \ word, + at which the parsed substring starts. + + When the call completes, \a curPos represents the offset, from the + first character of word, that is the successor of the offset of + the last parsed character. + + If the return value of the call is \c true, it is guaranteed that + the prefix of the substring of \word that contains the characters + from the initial value of \a curPos and up to but not including \a + curPos qualifies for auto-linking. + + If \a curPos is initially zero, the considered substring is the + entirety of \a word. +*/ +bool DocParser::isAutoLinkString(const QString &word, qsizetype &curPos) +{ + qsizetype len = word.size(); + qsizetype startPos = curPos; + int numUppercase = 0; + int numLowercase = 0; + int numStrangeSymbols = 0; + + while (curPos < len) { + unsigned char latin1Ch = word.at(curPos).toLatin1(); + if (islower(latin1Ch)) { + ++numLowercase; + ++curPos; + } else if (isupper(latin1Ch)) { + if (curPos > startPos) + ++numUppercase; + ++curPos; + } else if (isdigit(latin1Ch)) { + if (curPos > startPos) + ++curPos; + else + break; + } else if (latin1Ch == '_' || latin1Ch == '@') { + ++numStrangeSymbols; + ++curPos; + } else if ((latin1Ch == ':') && (curPos < len - 1) + && (word.at(curPos + 1) == QLatin1Char(':'))) { + ++numStrangeSymbols; + curPos += 2; + } else if (latin1Ch == '(') { + if ((curPos < len - 1) && (word.at(curPos + 1) == QLatin1Char(')'))) { + ++numStrangeSymbols; + m_position += 2; + } + + break; + } else { + break; + } + } + + return ((numUppercase >= 1 && numLowercase >= 2) || (numStrangeSymbols > 0 && (numUppercase + numLowercase >= 1))); +} + +bool DocParser::closeCommand(int endCmd) +{ + if (endCmdFor(m_openedCommands.top()) == endCmd && m_openedCommands.size() > 1) { + m_openedCommands.pop(); + return true; + } else { + bool contains = false; + QStack<int> opened2 = m_openedCommands; + while (opened2.size() > 1) { + if (endCmdFor(opened2.top()) == endCmd) { + contains = true; + break; + } + opened2.pop(); + } + + if (contains) { + while (endCmdFor(m_openedCommands.top()) != endCmd && m_openedCommands.size() > 1) { + location().warning( + QStringLiteral("Missing '\\%1' before '\\%2'") + .arg(endCmdName(m_openedCommands.top()), cmdName(endCmd))); + m_openedCommands.pop(); + } + } else { + location().warning(QStringLiteral("Unexpected '\\%1'").arg(cmdName(endCmd))); + } + return false; + } +} + +void DocParser::startSection(Doc::Sections unit, int cmd) +{ + leaveValueList(); + + if (m_currentSection == Doc::NoSection) { + m_currentSection = static_cast<Doc::Sections>(unit); + m_private->constructExtra(); + } else { + endSection(unit, cmd); + } + + appendAtom(Atom(Atom::SectionLeft, QString::number(unit))); + m_private->constructExtra(); + m_private->extra->m_tableOfContents.append(m_private->m_text.lastAtom()); + m_private->extra->m_tableOfContentsLevels.append(unit); + enterPara(Atom::SectionHeadingLeft, Atom::SectionHeadingRight, QString::number(unit)); + m_currentSection = unit; +} + +void DocParser::endSection(int, int) // (int unit, int endCmd) +{ + leavePara(); + appendAtom(Atom(Atom::SectionRight, QString::number(m_currentSection))); + m_currentSection = (Doc::NoSection); +} + +/*! + \internal + \brief Parses arguments to QDoc's see also command. + + Parses space or comma separated arguments passed to the \\sa command. + Multi-line input requires that the arguments are comma separated. Wrap + arguments in curly braces for multi-word targets, and for scope resolution + (for example, {QString::}{count()}). + + This method updates the list of links for the See also section. + + \sa {DocPrivate::}{addAlso()}, getArgument() + */ +void DocParser::parseAlso() +{ + auto line_comment = [this]() -> bool { + skipSpacesOnLine(); + if (m_position + 2 > m_inputLength) + return false; + if (m_input[m_position].unicode() == '/') { + if (m_input[m_position + 1].unicode() == '/') { + if (m_input[m_position + 2].unicode() == '!') { + return true; + } + } + } + return false; + }; + + auto skip_everything_until_newline = [this]() -> void { + while (m_position < m_inputLength && m_input[m_position] != '\n') + ++m_position; + }; + + leavePara(); + skipSpacesOnLine(); + while (m_position < m_inputLength && m_input[m_position] != '\n') { + QString target; + QString str; + bool skipMe = false; + + if (m_input[m_position] == '{') { + target = getArgument(); + skipSpacesOnLine(); + if (m_position < m_inputLength && m_input[m_position] == '{') { + str = getArgument(); + + // hack for C++ to support links like \l{QString::}{count()} + if (target.endsWith("::")) + target += str; + } else { + str = target; + } + } else { + target = getArgument(); + str = cleanLink(target); + if (target == QLatin1String("and") || target == QLatin1String(".")) + skipMe = true; + } + + if (!skipMe) { + Text also; + also << Atom(Atom::Link, target) << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) + << str << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); + m_private->addAlso(also); + } + + skipSpacesOnLine(); + + if (line_comment()) + skip_everything_until_newline(); + + if (m_position < m_inputLength && m_input[m_position] == ',') { + m_position++; + if (line_comment()) + skip_everything_until_newline(); + skipSpacesOrOneEndl(); + } else if (m_position >= m_inputLength || m_input[m_position] != '\n') { + location().warning(QStringLiteral("Missing comma in '\\%1'").arg(cmdName(CMD_SA))); + } + } +} + +void DocParser::appendAtom(const Atom& atom) { + m_private->m_text << atom; +} + +void DocParser::appendAtom(const LinkAtom& atom) { + m_private->m_text << atom; +} + +void DocParser::appendChar(QChar ch) +{ + if (m_private->m_text.lastAtom()->type() != Atom::String) + appendAtom(Atom(Atom::String)); + Atom *atom = m_private->m_text.lastAtom(); + if (ch == QLatin1Char(' ')) { + if (!atom->string().endsWith(QLatin1Char(' '))) + atom->appendChar(QLatin1Char(' ')); + } else + atom->appendChar(ch); +} + +void DocParser::appendWord(const QString &word) +{ + if (m_private->m_text.lastAtom()->type() != Atom::String) { + appendAtom(Atom(Atom::String, word)); + } else + m_private->m_text.lastAtom()->concatenateString(word); +} + +void DocParser::appendToCode(const QString &markedCode) +{ + if (!isCode(m_lastAtom)) { + appendAtom(Atom(Atom::Code)); + m_lastAtom = m_private->m_text.lastAtom(); + } + m_lastAtom->concatenateString(markedCode); +} + +void DocParser::appendToCode(const QString &markedCode, Atom::AtomType defaultType) +{ + if (!isCode(m_lastAtom)) { + appendAtom(Atom(defaultType, markedCode)); + m_lastAtom = m_private->m_text.lastAtom(); + } else { + m_lastAtom->concatenateString(markedCode); + } +} + +void DocParser::enterPara(Atom::AtomType leftType, Atom::AtomType rightType, const QString &string) +{ + if (m_paragraphState != OutsideParagraph) + return; + + if ((m_private->m_text.lastAtom()->type() != Atom::ListItemLeft) + && (m_private->m_text.lastAtom()->type() != Atom::DivLeft) + && (m_private->m_text.lastAtom()->type() != Atom::DetailsLeft)) { + leaveValueList(); + } + + appendAtom(Atom(leftType, string)); + m_indexStartedParagraph = false; + m_pendingParagraphLeftType = leftType; + m_pendingParagraphRightType = rightType; + m_pendingParagraphString = string; + if (leftType == Atom::SectionHeadingLeft) { + m_paragraphState = InSingleLineParagraph; + } else { + m_paragraphState = InMultiLineParagraph; + } + skipSpacesOrOneEndl(); +} + +void DocParser::leavePara() +{ + if (m_paragraphState == OutsideParagraph) + return; + + if (!m_pendingFormats.isEmpty()) { + location().warning(QStringLiteral("Missing '}'")); + m_pendingFormats.clear(); + } + + if (m_private->m_text.lastAtom()->type() == m_pendingParagraphLeftType) { + m_private->m_text.stripLastAtom(); + } else { + if (m_private->m_text.lastAtom()->type() == Atom::String + && m_private->m_text.lastAtom()->string().endsWith(QLatin1Char(' '))) { + m_private->m_text.lastAtom()->chopString(); + } + appendAtom(Atom(m_pendingParagraphRightType, m_pendingParagraphString)); + } + m_paragraphState = OutsideParagraph; + m_indexStartedParagraph = false; + m_pendingParagraphRightType = Atom::Nop; + m_pendingParagraphString.clear(); +} + +void DocParser::leaveValue() +{ + leavePara(); + if (m_openedLists.isEmpty()) { + m_openedLists.push(OpenedList(OpenedList::Value)); + appendAtom(Atom(Atom::ListLeft, ATOM_LIST_VALUE)); + } else { + if (m_private->m_text.lastAtom()->type() == Atom::Nop) + m_private->m_text.stripLastAtom(); + appendAtom(Atom(Atom::ListItemRight, ATOM_LIST_VALUE)); + } +} + +void DocParser::leaveValueList() +{ + leavePara(); + if (!m_openedLists.isEmpty() && (m_openedLists.top().style() == OpenedList::Value)) { + if (m_private->m_text.lastAtom()->type() == Atom::Nop) + m_private->m_text.stripLastAtom(); + appendAtom(Atom(Atom::ListItemRight, ATOM_LIST_VALUE)); + appendAtom(Atom(Atom::ListRight, ATOM_LIST_VALUE)); + m_openedLists.pop(); + } +} + +void DocParser::leaveTableRow() +{ + if (m_inTableItem) { + leavePara(); + appendAtom(Atom(Atom::TableItemRight)); + m_inTableItem = false; + } + if (m_inTableHeader) { + appendAtom(Atom(Atom::TableHeaderRight)); + m_inTableHeader = false; + } + if (m_inTableRow) { + appendAtom(Atom(Atom::TableRowRight)); + m_inTableRow = false; + } +} + +void DocParser::quoteFromFile(const QString &filename) +{ + // KLUDGE: We dereference file_resolver as it is temporarily a pointer. + // See the comment for file_resolver in the header files for more context. + // + // We spefically dereference it, instead of using the arrow + // operator, to better represent that we do not consider this as + // an actual pointer, as it should not be. + // + // Do note that we are considering it informally safe to + // dereference the pointer, as we expect it to always hold a value + // at this point, but actual enforcement of this appears nowhere + // in the codebase. + auto maybe_resolved_file{(*file_resolver).resolve(filename)}; + if (!maybe_resolved_file) { + // TODO: [uncentralized-admonition][failed-resolve-file] + // This warning is required in multiple places. + // To ensure the consistency of the warning and avoid + // duplicating code everywhere, provide a centralized effort + // where the warning message can be generated (but not + // issued). + // The current format is based on what was used before, review + // it when it is moved out. + QString details = std::transform_reduce( + (*file_resolver).get_search_directories().cbegin(), + (*file_resolver).get_search_directories().cend(), + u"Searched directories:"_s, + std::plus(), + [](const DirectoryPath &directory_path) -> QString { return u' ' + directory_path.value(); } + ); + + location().warning(u"Cannot find file to quote from: %1"_s.arg(filename), details); + + // REMARK: The following is duplicated from + // Doc::quoteFromFile. If, for some reason (such as a file + // that is inaccessible), the quoting fails but, previously, + // the logic duplicated here was still run. + // This is not true anymore as quoteFromFile does require a + // resolved file to be run now. + // It is not entirely clear if this is required for the + // semantics of DocParser to be preserved, but for the sake of + // avoiding premature breakages this was retained. + // Do note that this should be considered temporary as the + // quoter state, if any will be preserved, should not be + // managed in such a spread and unlocal way. + m_quoter.reset(); + + CodeMarker *marker = CodeMarker::markerForFileName(QString{}); + m_quoter.quoteFromFile(filename, QString{}, marker->markedUpCode(QString{}, nullptr, location())); + } else Doc::quoteFromFile(location(), m_quoter, *maybe_resolved_file); +} + +/*! + Expands a macro in-place in input. + + Expects the current \e pos in the input to point to a backslash, and the macro to have a + default definition. Format-specific macros are not expanded. + + Behavior depends on \a options: + + \value ArgumentParsingOptions::Default + Default macro expansion; the string following the backslash + must be a macro with a default definition. + \value ArgumentParsingOptions::Verbatim + The string following the backslash is rendered verbatim; + No macro expansion is performed. + \value ArgumentParsingOptions::MacroArguments + Used for parsing argument(s) for a macro. Allows expanding + macros, and also preserves a subset of commands (formatting + commands) within the macro argument. + + \note In addition to macros, a valid use for a backslash in an argument include + escaping non-alnum characters, and splitting a single argument across multiple + lines by escaping newlines. Escaping is also handled here. + + Returns \c true on successful macro expansion. + */ +bool DocParser::expandMacro(ArgumentParsingOptions options) +{ + Q_ASSERT(m_input[m_position].unicode() == '\\'); + + if (options == ArgumentParsingOptions::Verbatim) + return false; + + QString cmdStr; + qsizetype backslashPos = m_position++; + while (m_position < m_input.size() && m_input[m_position].isLetterOrNumber()) + cmdStr += m_input[m_position++]; + + m_endPosition = m_position; + if (!cmdStr.isEmpty()) { + if (s_utilities.macroHash.contains(cmdStr)) { + const Macro ¯o = s_utilities.macroHash.value(cmdStr); + if (!macro.m_defaultDef.isEmpty()) { + QString expanded = expandMacroToString(cmdStr, macro); + m_input.replace(backslashPos, m_position - backslashPos, expanded); + m_inputLength = m_input.size(); + m_position = backslashPos; + return true; + } else { + location().warning("Macro '%1' does not have a default definition"_L1.arg(cmdStr)); + } + } else { + int cmd = s_utilities.cmdHash.value(cmdStr, NOT_A_CMD); + m_position = backslashPos; + if (options != ArgumentParsingOptions::MacroArguments + || cmd == NOT_A_CMD || !cmds[cmd].is_formatting_command) { + location().warning("Unknown macro '%1'"_L1.arg(cmdStr)); + ++m_position; + } + } + } else if (m_input[m_position].isSpace()) { + skipAllSpaces(); + } else if (m_input[m_position].unicode() == '\\') { + // allow escaping a backslash + m_input.remove(m_position--, 1); + --m_inputLength; + } + return false; +} + +void DocParser::expandMacro(const QString &def, const QStringList &args) +{ + if (args.isEmpty()) { + appendAtom(Atom(Atom::RawString, def)); + } else { + QString rawString; + + for (int j = 0; j < def.size(); ++j) { + if (int paramNo = def[j].unicode(); paramNo >= 1 && paramNo <= args.length()) { + if (!rawString.isEmpty()) { + appendAtom(Atom(Atom::RawString, rawString)); + rawString.clear(); + } + appendAtom(Atom(Atom::String, args[paramNo - 1])); + } else { + rawString += def[j]; + } + } + if (!rawString.isEmpty()) + appendAtom(Atom(Atom::RawString, rawString)); + } +} + +QString DocParser::expandMacroToString(const QString &name, const Macro ¯o) +{ + const QString &def{macro.m_defaultDef}; + QString rawString; + + if (macro.numParams == 0) { + rawString = macro.m_defaultDef; + } else { + QStringList args{getMacroArguments(name, macro)}; + + for (int j = 0; j < def.size(); ++j) { + int paramNo = def[j].unicode(); + rawString += (paramNo >= 1 && paramNo <= args.length()) ? args[paramNo - 1] : def[j]; + } + } + QString matchExpr{macro.m_otherDefs.value("match")}; + if (matchExpr.isEmpty()) + return rawString; + + QString result; + QRegularExpression re(matchExpr); + int capStart = (re.captureCount() > 0) ? 1 : 0; + qsizetype i = 0; + QRegularExpressionMatch match; + while ((match = re.match(rawString, i)).hasMatch()) { + for (int c = capStart; c <= re.captureCount(); ++c) + result += match.captured(c); + i = match.capturedEnd(); + } + + return result; +} + +Doc::Sections DocParser::getSectioningUnit() +{ + QString name = getOptionalArgument(); + + if (name == "section1") { + return Doc::Section1; + } else if (name == "section2") { + return Doc::Section2; + } else if (name == "section3") { + return Doc::Section3; + } else if (name == "section4") { + return Doc::Section4; + } else if (name.isEmpty()) { + return Doc::NoSection; + } else { + location().warning(QStringLiteral("Invalid section '%1'").arg(name)); + return Doc::NoSection; + } +} + +/*! + Gets an argument that is enclosed in braces and returns it + without the enclosing braces. On entry, the current character + is the left brace. On exit, the current character is the one + that comes after the right brace. + + If \a options is ArgumentParsingOptions::Verbatim, no macro + expansion is performed, nor is the returned string stripped + of extra whitespace. + */ +QString DocParser::getBracedArgument(ArgumentParsingOptions options) +{ + QString arg; + int delimDepth = 0; + if (m_position < m_input.size() && m_input[m_position] == '{') { + ++m_position; + while (m_position < m_input.size() && delimDepth >= 0) { + switch (m_input[m_position].unicode()) { + case '{': + ++delimDepth; + arg += QLatin1Char('{'); + ++m_position; + break; + case '}': + --delimDepth; + if (delimDepth >= 0) + arg += QLatin1Char('}'); + ++m_position; + break; + case '\\': + if (!expandMacro(options)) + arg += m_input[m_position++]; + break; + default: + if (m_input[m_position].isSpace() && options != ArgumentParsingOptions::Verbatim) + arg += QChar(' '); + else + arg += m_input[m_position]; + ++m_position; + } + } + if (delimDepth > 0) + location().warning(QStringLiteral("Missing '}'")); + } + m_endPosition = m_position; + return arg; +} + +/*! + Parses and returns an argument for a command, using + specific parsing \a options. + + Typically, an argument ends at the next white-space. However, + braces can be used to group words: + + {a few words} + + Also, opening and closing parentheses have to match. Thus, + + printf("%d\n", x) + + is an argument too, although it contains spaces. Finally, + trailing punctuation is not included in an argument, nor is 's. +*/ +QString DocParser::getArgument(ArgumentParsingOptions options) +{ + skipSpacesOrOneEndl(); + + int delimDepth = 0; + qsizetype startPos = m_position; + QString arg = getBracedArgument(options); + if (arg.isEmpty()) { + while ((m_position < m_input.size()) + && ((delimDepth > 0) || ((delimDepth == 0) && !m_input[m_position].isSpace()))) { + switch (m_input[m_position].unicode()) { + case '(': + case '[': + case '{': + ++delimDepth; + arg += m_input[m_position]; + ++m_position; + break; + case ')': + case ']': + case '}': + --delimDepth; + if (m_position == startPos || delimDepth >= 0) { + arg += m_input[m_position]; + ++m_position; + } + break; + case '\\': + if (!expandMacro(options)) + arg += m_input[m_position++]; + break; + default: + arg += m_input[m_position]; + ++m_position; + } + } + m_endPosition = m_position; + if ((arg.size() > 1) && (QString(".,:;!?").indexOf(m_input[m_position - 1]) != -1) + && !arg.endsWith("...")) { + arg.truncate(arg.size() - 1); + --m_position; + } + if (arg.size() > 2 && m_input.mid(m_position - 2, 2) == "'s") { + arg.truncate(arg.size() - 2); + m_position -= 2; + } + } + return arg.simplified(); +} + +/*! + Gets an argument that is enclosed in brackets and returns it + without the enclosing brackets. On entry, the current character + is the left bracket. On exit, the current character is the one + that comes after the right bracket. + */ +QString DocParser::getBracketedArgument() +{ + QString arg; + int delimDepth = 0; + skipSpacesOrOneEndl(); + if (m_position < m_input.size() && m_input[m_position] == '[') { + ++m_position; + while (m_position < m_input.size() && delimDepth >= 0) { + switch (m_input[m_position].unicode()) { + case '[': + ++delimDepth; + arg += QLatin1Char('['); + ++m_position; + break; + case ']': + --delimDepth; + if (delimDepth >= 0) + arg += QLatin1Char(']'); + ++m_position; + break; + case '\\': + arg += m_input[m_position]; + ++m_position; + break; + default: + arg += m_input[m_position]; + ++m_position; + } + } + if (delimDepth > 0) + location().warning(QStringLiteral("Missing ']'")); + } + return arg; +} + + +/*! + Returns the list of arguments passed to a \a macro with name \a name. + + If a macro takes more than a single argument, they are expected to be + wrapped in braces. +*/ +QStringList DocParser::getMacroArguments(const QString &name, const Macro ¯o) +{ + QStringList args; + for (int i = 0; i < macro.numParams; ++i) { + if (macro.numParams == 1 || isLeftBraceAhead()) { + args << getArgument(ArgumentParsingOptions::MacroArguments); + } else { + location().warning(QStringLiteral("Macro '\\%1' invoked with too few" + " arguments (expected %2, got %3)") + .arg(name) + .arg(macro.numParams) + .arg(i)); + break; + } + } + return args; +} + +QString DocParser::getOptionalArgument() +{ + skipSpacesOrOneEndl(); + if (m_position + 1 < m_input.size() && m_input[m_position] == '\\' + && m_input[m_position + 1].isLetterOrNumber()) { + return QString(); + } else { + return getArgument(); + } +} + +/*! + \brief Create a string that may optionally span multiple lines as one line. + + Process a block of text that may span multiple lines using trailing + backslashes (`\`) as line continuation character. Trailing backslashes and + any newline character that follow them are removed. + + Returns a string as if it was one continuous line of text. If trailing + backslashes are removed, the method returns a "simplified" QString, which + means any sequence of internal whitespace is replaced with a single space. + + Whitespace at the start and end is always removed from the returned string. + + \sa QString::simplified(), QString::trimmed(). + */ +QString DocParser::getRestOfLine() +{ + auto lineHasTrailingBackslash = [this](bool trailingBackslash) -> bool { + while (m_position < m_inputLength && m_input[m_position] != '\n') { + if (m_input[m_position] == '\\' && !trailingBackslash) { + trailingBackslash = true; + ++m_position; + skipSpacesOnLine(); + } else { + trailingBackslash = false; + ++m_position; + } + } + return trailingBackslash; + }; + + QString rest_of_line; + skipSpacesOnLine(); + bool trailing_backslash{ false }; + bool return_simplified_string{ false }; + + for (qsizetype start_position = m_position; m_position < m_inputLength; ++m_position) { + trailing_backslash = lineHasTrailingBackslash(trailing_backslash); + + if (!rest_of_line.isEmpty()) + rest_of_line += QLatin1Char(' '); + rest_of_line += m_input.sliced(start_position, m_position - start_position); + + if (trailing_backslash) { + rest_of_line.truncate(rest_of_line.lastIndexOf('\\')); + return_simplified_string = true; + } + + if (m_position < m_inputLength) + ++m_position; + + if (!trailing_backslash) + break; + start_position = m_position; + } + + if (return_simplified_string) + return rest_of_line.simplified(); + + return rest_of_line.trimmed(); +} + +/*! + The metacommand argument is normally the remaining text to + the right of the metacommand itself. The extra blanks are + stripped and the argument string is returned. + */ +QString DocParser::getMetaCommandArgument(const QString &cmdStr) +{ + skipSpacesOnLine(); + + qsizetype begin = m_position; + int parenDepth = 0; + + while (m_position < m_input.size() && (m_input[m_position] != '\n' || parenDepth > 0)) { + if (m_input.at(m_position) == '(') + ++parenDepth; + else if (m_input.at(m_position) == ')') + --parenDepth; + else if (m_input.at(m_position) == '\\' && expandMacro(ArgumentParsingOptions::Default)) + continue; + ++m_position; + } + if (m_position == m_input.size() && parenDepth > 0) { + m_position = begin; + location().warning(QStringLiteral("Unbalanced parentheses in '%1'").arg(cmdStr)); + } + + QString t = m_input.mid(begin, m_position - begin).simplified(); + skipSpacesOnLine(); + return t; +} + +QString DocParser::getUntilEnd(int cmd) +{ + int endCmd = endCmdFor(cmd); + QRegularExpression rx("\\\\" + cmdName(endCmd) + "\\b"); + QString t; + auto match = rx.match(m_input, m_position); + + if (!match.hasMatch()) { + location().warning(QStringLiteral("Missing '\\%1'").arg(cmdName(endCmd))); + m_position = m_input.size(); + } else { + qsizetype end = match.capturedStart(); + t = m_input.mid(m_position, end - m_position); + m_position = match.capturedEnd(); + } + return t; +} + +void DocParser::expandArgumentsInString(QString &str, const QStringList &args) +{ + if (args.isEmpty()) + return; + + qsizetype paramNo; + qsizetype j = 0; + while (j < str.size()) { + if (str[j] == '\\' && j < str.size() - 1 && (paramNo = str[j + 1].digitValue()) >= 1 + && paramNo <= args.size()) { + const QString &r = args[paramNo - 1]; + str.replace(j, 2, r); + j += qMin(1, r.size()); + } else { + ++j; + } + } +} + +/*! + Returns the marked-up code following the code-quoting command \a cmd, expanding + any arguments passed in \a argStr. + + Uses the \a marker to mark up the code. If it's \c nullptr, resolve the marker + based on the topic and the quoted code itself. +*/ +QString DocParser::getCode(int cmd, CodeMarker *marker, const QString &argStr) +{ + QString code = untabifyEtc(getUntilEnd(cmd)); + expandArgumentsInString(code, argStr.split(" ", Qt::SkipEmptyParts)); + + int indent = indentLevel(code); + code = dedent(indent, code); + + // If we're in a QML topic, check if the QML marker recognizes the code + if (!marker && !m_private->m_topics.isEmpty() + && m_private->m_topics[0].m_topic.startsWith("qml")) { + auto qmlMarker = CodeMarker::markerForLanguage("QML"); + marker = (qmlMarker && qmlMarker->recognizeCode(code)) ? qmlMarker : nullptr; + } + if (marker == nullptr) + marker = CodeMarker::markerForCode(code); + return marker->markedUpCode(code, nullptr, location()); +} + +bool DocParser::isBlankLine() +{ + qsizetype i = m_position; + + while (i < m_inputLength && m_input[i].isSpace()) { + if (m_input[i] == '\n') + return true; + ++i; + } + return false; +} + +bool DocParser::isLeftBraceAhead() +{ + int numEndl = 0; + qsizetype i = m_position; + + while (i < m_inputLength && m_input[i].isSpace() && numEndl < 2) { + // ### bug with '\\' + if (m_input[i] == '\n') + numEndl++; + ++i; + } + return numEndl < 2 && i < m_inputLength && m_input[i] == '{'; +} + +bool DocParser::isLeftBracketAhead() +{ + int numEndl = 0; + qsizetype i = m_position; + + while (i < m_inputLength && m_input[i].isSpace() && numEndl < 2) { + // ### bug with '\\' + if (m_input[i] == '\n') + numEndl++; + ++i; + } + return numEndl < 2 && i < m_inputLength && m_input[i] == '['; +} + +/*! + Skips to the next non-space character or EOL. + */ +void DocParser::skipSpacesOnLine() +{ + while ((m_position < m_input.size()) && m_input[m_position].isSpace() + && (m_input[m_position].unicode() != '\n')) + ++m_position; +} + +/*! + Skips spaces and one EOL. + */ +void DocParser::skipSpacesOrOneEndl() +{ + qsizetype firstEndl = -1; + while (m_position < m_input.size() && m_input[m_position].isSpace()) { + QChar ch = m_input[m_position]; + if (ch == '\n') { + if (firstEndl == -1) { + firstEndl = m_position; + } else { + m_position = firstEndl; + break; + } + } + ++m_position; + } +} + +void DocParser::skipAllSpaces() +{ + while (m_position < m_inputLength && m_input[m_position].isSpace()) + ++m_position; +} + +void DocParser::skipToNextPreprocessorCommand() +{ + QRegularExpression rx("\\\\(?:" + cmdName(CMD_IF) + QLatin1Char('|') + cmdName(CMD_ELSE) + + QLatin1Char('|') + cmdName(CMD_ENDIF) + ")\\b"); + auto match = rx.match(m_input, m_position + 1); // ### + 1 necessary? + + if (!match.hasMatch()) + m_position = m_input.size(); + else + m_position = match.capturedStart(); +} + +int DocParser::endCmdFor(int cmd) +{ + switch (cmd) { + case CMD_BADCODE: + return CMD_ENDCODE; + case CMD_CODE: + return CMD_ENDCODE; + case CMD_COMPARESWITH: + return CMD_ENDCOMPARESWITH; + case CMD_DETAILS: + return CMD_ENDDETAILS; + case CMD_DIV: + return CMD_ENDDIV; + case CMD_QML: + return CMD_ENDQML; + case CMD_FOOTNOTE: + return CMD_ENDFOOTNOTE; + case CMD_LEGALESE: + return CMD_ENDLEGALESE; + case CMD_LINK: + return CMD_ENDLINK; + case CMD_LIST: + return CMD_ENDLIST; + case CMD_OMIT: + return CMD_ENDOMIT; + case CMD_QUOTATION: + return CMD_ENDQUOTATION; + case CMD_RAW: + return CMD_ENDRAW; + case CMD_SECTION1: + return CMD_ENDSECTION1; + case CMD_SECTION2: + return CMD_ENDSECTION2; + case CMD_SECTION3: + return CMD_ENDSECTION3; + case CMD_SECTION4: + return CMD_ENDSECTION4; + case CMD_SIDEBAR: + return CMD_ENDSIDEBAR; + case CMD_TABLE: + return CMD_ENDTABLE; + default: + return cmd; + } +} + +QString DocParser::cmdName(int cmd) +{ + return cmds[cmd].name; +} + +QString DocParser::endCmdName(int cmd) +{ + return cmdName(endCmdFor(cmd)); +} + +QString DocParser::untabifyEtc(const QString &str) +{ + QString result; + result.reserve(str.size()); + int column = 0; + + for (const auto &character : str) { + if (character == QLatin1Char('\r')) + continue; + if (character == QLatin1Char('\t')) { + result += &" "[column % s_tabSize]; + column = ((column / s_tabSize) + 1) * s_tabSize; + continue; + } + if (character == QLatin1Char('\n')) { + while (result.endsWith(QLatin1Char(' '))) + result.chop(1); + result += character; + column = 0; + continue; + } + result += character; + ++column; + } + + while (result.endsWith("\n\n")) + result.truncate(result.size() - 1); + while (result.startsWith(QLatin1Char('\n'))) + result = result.mid(1); + + return result; +} + +int DocParser::indentLevel(const QString &str) +{ + int minIndent = INT_MAX; + int column = 0; + + for (const auto &character : str) { + if (character == '\n') { + column = 0; + } else { + if (character != ' ' && column < minIndent) + minIndent = column; + ++column; + } + } + return minIndent; +} + +QString DocParser::dedent(int level, const QString &str) +{ + if (level == 0) + return str; + + QString result; + int column = 0; + + for (const auto &character : str) { + if (character == QLatin1Char('\n')) { + result += '\n'; + column = 0; + } else { + if (column >= level) + result += character; + ++column; + } + } + return result; +} + +/*! + Returns \c true if \a atom represents a code snippet. + */ +bool DocParser::isCode(const Atom *atom) +{ + Atom::AtomType type = atom->type(); + return (type == Atom::Code || type == Atom::Qml); +} + +/*! + Returns \c true if \a atom represents quoting information. + */ +bool DocParser::isQuote(const Atom *atom) +{ + Atom::AtomType type = atom->type(); + return (type == Atom::CodeQuoteArgument || type == Atom::CodeQuoteCommand + || type == Atom::SnippetCommand || type == Atom::SnippetIdentifier + || type == Atom::SnippetLocation); +} + +/*! + \internal + Processes the arguments passed to the \\compareswith block command. + The arguments are stored as text within the first atom of the block + (Atom::ComparesLeft). + + Extracts the comparison category and the list of types, and stores + the information into a map accessed via \a priv. +*/ +static void processComparesWithCommand(DocPrivate *priv, const Location &location) +{ + static auto take_while = [](QStringView input, auto predicate) { + QStringView::size_type end{0}; + + while (end < input.size() && std::invoke(predicate, input[end])) + ++end; + + return std::make_tuple(input.sliced(0, end), input.sliced(end)); + }; + + static auto peek = [](QStringView input, QChar c) { + return !input.empty() && input.first() == c; + }; + + static auto skip_one = [](QStringView input) { + if (input.empty()) return std::make_tuple(QStringView{}, input); + else return std::make_tuple(input.sliced(0, 1), input.sliced(1)); + }; + + static auto enclosed = [](QStringView input, QChar open, QChar close) { + if (!peek(input, open)) return std::make_tuple(QStringView{}, input); + + auto [opened, without_open] = skip_one(input); + auto [parsed, remaining] = take_while(without_open, [close](QChar c){ return c != close; }); + + if (remaining.empty()) return std::make_tuple(QStringView{}, input); + + auto [closed, without_close] = skip_one(remaining); + + return std::make_tuple(parsed.trimmed(), without_close); + }; + + static auto one_of = [](auto first, auto second) { + return [first, second](QStringView input) { + auto [parsed, remaining] = std::invoke(first, input); + + if (parsed.empty()) return std::invoke(second, input); + else return std::make_tuple(parsed, remaining); + }; + }; + + static auto collect = [](QStringView input, auto parser) { + QStringList collected{}; + + while (true) { + auto [parsed, remaining] = std::invoke(parser, input); + + if (parsed.empty()) break; + collected.append(parsed.toString()); + + input = remaining; + }; + + return collected; + }; + + static auto spaces = [](QStringView input) { + return take_while(input, [](QChar c){ return c.isSpace(); }); + }; + + static auto word = [](QStringView input) { + return take_while(input, [](QChar c){ return !c.isSpace(); }); + }; + + static auto parse_argument = [](QStringView input) { + auto [_, without_spaces] = spaces(input); + + return one_of( + [](QStringView input){ return enclosed(input, '{', '}'); }, + word + )(without_spaces); + }; + + const QString cmd{DocParser::cmdName(CMD_COMPARESWITH)}; + Text text = priv->m_text.splitAtFirst(Atom::ComparesLeft); + + auto *atom = text.firstAtom(); + QStringList segments = collect(atom->string(), parse_argument); + + QString categoryString; + if (!segments.isEmpty()) + categoryString = segments.takeFirst(); + auto category = comparisonCategoryFromString(categoryString.toStdString()); + + if (category == ComparisonCategory::None) { + location.warning(u"Invalid argument to \\%1 command: `%2`"_s.arg(cmd, categoryString), + u"Valid arguments are `strong`, `weak`, `partial`, or `equality`."_s); + return; + } + + if (segments.isEmpty()) { + location.warning(u"Missing argument to \\%1 command."_s.arg(cmd), + u"Provide at least one type name, or a list of types separated by spaces."_s); + return; + } + + // Store cleaned-up type names back into the atom + segments.removeDuplicates(); + atom->setString(segments.join(QLatin1Char(';'))); + + // Add an entry to meta-command map for error handling in CppCodeParser + priv->m_metaCommandMap[cmd].append(ArgPair(categoryString, atom->string())); + priv->m_metacommandsUsed.insert(cmd); + + priv->constructExtra(); + const auto end{priv->extra->m_comparesWithMap.cend()}; + priv->extra->m_comparesWithMap.insert(end, category, text); +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/docparser.h b/src/qdoc/qdoc/src/qdoc/docparser.h new file mode 100644 index 000000000..c577e12ba --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/docparser.h @@ -0,0 +1,176 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +#ifndef DOCPARSER_H +#define DOCPARSER_H + +#include "atom.h" +#include "config.h" +#include "docutilities.h" +#include "location.h" +#include "openedlist.h" +#include "quoter.h" + +#include "filesystem/fileresolver.h" + +#include <QtCore/QCoreApplication> +#include <QtCore/qglobalstatic.h> +#include <QtCore/qhash.h> +#include <QtCore/qstack.h> +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +class Doc; +class DocPrivate; +class CodeMarker; +struct Macro; + +class DocParser +{ +public: + void parse(const QString &source, DocPrivate *docPrivate, const QSet<QString> &metaCommandSet, + const QSet<QString> &possibleTopics); + + static void initialize(const Config &config, FileResolver& file_resolver); + static int endCmdFor(int cmd); + static QString cmdName(int cmd); + static QString endCmdName(int cmd); + static QString untabifyEtc(const QString &str); + static int indentLevel(const QString &str); + static QString dedent(int level, const QString &str); + + static int s_tabSize; + static QStringList s_ignoreWords; + static bool s_quoting; + +private: + + enum class ArgumentParsingOptions { + Default, + Verbatim, + MacroArguments + }; + + Location &location(); + QString detailsUnknownCommand(const QSet<QString> &metaCommandSet, const QString &str); + void insertTarget(const QString &target); + void insertKeyword(const QString &keyword); + void include(const QString &fileName, const QString &identifier, const QStringList ¶meters); + void startFormat(const QString &format, int cmd); + bool openCommand(int cmd); + bool closeCommand(int endCmd); + void startSection(Doc::Sections unit, int cmd); + void endSection(int unit, int endCmd); + void parseAlso(); + void appendAtom(const Atom&); + void appendAtom(const LinkAtom&); + void appendChar(QChar ch); + void appendWord(const QString &word); + void appendToCode(const QString &code); + void appendToCode(const QString &code, Atom::AtomType defaultType); + void enterPara(Atom::AtomType leftType = Atom::ParaLeft, + Atom::AtomType rightType = Atom::ParaRight, const QString &string = QString()); + void leavePara(); + void leaveValue(); + void leaveValueList(); + void leaveTableRow(); + void quoteFromFile(const QString& filename); + bool expandMacro(ArgumentParsingOptions options); + void expandMacro(const QString &def, const QStringList &args); + QString expandMacroToString(const QString &name, const Macro ¯o); + Doc::Sections getSectioningUnit(); + QString getArgument(ArgumentParsingOptions options = ArgumentParsingOptions::Default); + QString getBracedArgument(ArgumentParsingOptions options); + QString getBracketedArgument(); + QStringList getMacroArguments(const QString &name, const Macro ¯o); + QString getOptionalArgument(); + QString getRestOfLine(); + QString getMetaCommandArgument(const QString &cmdStr); + QString getUntilEnd(int cmd); + QString getCode(int cmd, CodeMarker *marker, const QString &argStr = QString()); + + inline bool isAutoLinkString(const QString &word); + bool isAutoLinkString(const QString &word, qsizetype &curPos); + bool isBlankLine(); + bool isLeftBraceAhead(); + bool isLeftBracketAhead(); + void skipSpacesOnLine(); + void skipSpacesOrOneEndl(); + void skipAllSpaces(); + void skipToNextPreprocessorCommand(); + static bool isCode(const Atom *atom); + static bool isQuote(const Atom *atom); + static void expandArgumentsInString(QString &str, const QStringList &args); + + QStack<qsizetype> m_openedInputs {}; + + QString m_input {}; + qsizetype m_position {}; + qsizetype m_backslashPosition {}; + qsizetype m_endPosition {}; + qsizetype m_inputLength {}; + Location m_cachedLocation {}; + qsizetype m_cachedPosition {}; + + DocPrivate *m_private { nullptr }; + enum ParagraphState { OutsideParagraph, InSingleLineParagraph, InMultiLineParagraph }; + ParagraphState m_paragraphState {}; + bool m_inTableHeader {}; + bool m_inTableRow {}; + bool m_inTableItem {}; + bool m_indexStartedParagraph {}; // ### rename + Atom::AtomType m_pendingParagraphLeftType {}; + Atom::AtomType m_pendingParagraphRightType {}; + QString m_pendingParagraphString {}; + + int m_braceDepth {}; + Doc::Sections m_currentSection {}; + QMap<QString, Location> m_targetMap {}; + QMap<int, QString> m_pendingFormats {}; + QStack<int> m_openedCommands {}; + QStack<OpenedList> m_openedLists {}; + Quoter m_quoter {}; + Atom *m_lastAtom { nullptr }; + + static DocUtilities &s_utilities; + + // KLUDGE: When parsing documentation, there is a need to find + // files to resolve quoting commands. Ideally, the system that + // takes care of this would be a non-static member that is a + // reference that is passed at + // construction time. + // Nonetheless, with how the current codebase is constructed, this + // has proven to be extremely difficult until more changes are + // done. In particular, the construction of a DocParser happens in + // multiple places at multiple depths and, in particular, happens + // in one of Doc's constructor. + // Doc itself is built, again, in multiple places at multiple + // depths, making it clumsy and sometimes infeasible to pass the + // dependency around so that it is available at the required + // places. In particular, this stems from the fact that Doc is + // holding many responsabilities and is spread troughtout much of + // the codebase in different ways. DocParser mostly depends on Doc + // and Doc currently depends on DocParser, making the two + // difficult to separate. + // + // In the future, we expect Doc to mostly be removed, such as to + // remove this dependencies and the parsing of documentation to + // happen near main and atomically from other endevours, producing + // an intermediate representation that is consumed by later + // phases. + // At that point, it should be possible to not have this kind of + // indirection while, for now, the only accessible way to pass + // this dependency is trough the initialize method which passes + // for Doc::initialize. + // + // Furthemore, as we cannot late-bind a reference, and having a + // desire to avoid an unnecessary copy, we are thus forced to use + // a different storage method, in this case a pointer. + // This too should be removed later on, using reference or move + // semantic depending on the required data-flow. + static FileResolver* file_resolver; +}; + +QT_END_NAMESPACE + +#endif // DOCPARSER_H diff --git a/src/qdoc/qdoc/src/qdoc/docprivate.cpp b/src/qdoc/qdoc/src/qdoc/docprivate.cpp new file mode 100644 index 000000000..a7c178d57 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/docprivate.cpp @@ -0,0 +1,30 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +#include "docprivate.h" + +#include "text.h" + +#include <QtCore/qhash.h> + +QT_BEGIN_NAMESPACE + +/*! + Deletes the DocPrivateExtra. + */ +DocPrivate::~DocPrivate() +{ + delete extra; +} + +void DocPrivate::addAlso(const Text &also) +{ + m_alsoList.append(also); +} + +void DocPrivate::constructExtra() +{ + if (extra == nullptr) + extra = new DocPrivateExtra; +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/docprivate.h b/src/qdoc/qdoc/src/qdoc/docprivate.h new file mode 100644 index 000000000..7402290c9 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/docprivate.h @@ -0,0 +1,76 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +#ifndef DOCPRIVATE_H +#define DOCPRIVATE_H + +#include "atom.h" +#include "config.h" +#include "codemarker.h" +#include "doc.h" +#include "editdistance.h" +#include "generator.h" +#include "utilities.h" +#include "openedlist.h" +#include "quoter.h" +#include "text.h" +#include "tokenizer.h" + +#include <QtCore/qdatetime.h> +#include <QtCore/qfile.h> +#include <QtCore/qfileinfo.h> +#include <QtCore/qhash.h> +#include <QtCore/qmap.h> +#include <QtCore/qtextstream.h> + +#include <cctype> +#include <climits> +#include <utility> + +QT_BEGIN_NAMESPACE + +typedef QMap<QString, ArgList> CommandMap; + +struct DocPrivateExtra +{ + QList<Atom *> m_tableOfContents {}; + QList<int> m_tableOfContentsLevels {}; + QList<Atom *> m_keywords {}; + QList<Atom *> m_targets {}; + QStringMultiMap m_metaMap {}; + QMultiMap<ComparisonCategory, Text> m_comparesWithMap {}; +}; + +class DocPrivate +{ +public: + explicit DocPrivate(const Location &start = Location(), const Location &end = Location(), + QString source = QString()) + : m_start_loc(start), m_end_loc(end), m_src(std::move(source)), m_hasLegalese(false) {}; + ~DocPrivate(); + + void addAlso(const Text &also); + void constructExtra(); + void ref() { ++count; } + bool deref() { return (--count == 0); } + + int count { 1 }; + // ### move some of this in DocPrivateExtra + Location m_start_loc {}; + Location m_end_loc {}; + QString m_src {}; + Text m_text {}; + QSet<QString> m_params {}; + QList<Text> m_alsoList {}; + QStringList m_enumItemList {}; + QStringList m_omitEnumItemList {}; + QSet<QString> m_metacommandsUsed {}; + CommandMap m_metaCommandMap {}; + DocPrivateExtra *extra { nullptr }; + TopicList m_topics {}; + + bool m_hasLegalese : 1; +}; + +QT_END_NAMESPACE + +#endif // DOCPRIVATE_H diff --git a/src/qdoc/qdoc/src/qdoc/docutilities.h b/src/qdoc/qdoc/src/qdoc/docutilities.h new file mode 100644 index 000000000..d4483ac73 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/docutilities.h @@ -0,0 +1,28 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +#ifndef DOCUTILITIES_H +#define DOCUTILITIES_H + +#include "macro.h" +#include "singleton.h" + +#include <QtCore/qglobal.h> +#include <QtCore/qhash.h> +#include <QtCore/qstring.h> +#include <QtCore/qmap.h> + +QT_BEGIN_NAMESPACE + +typedef QHash<QString, int> QHash_QString_int; +typedef QHash<QString, Macro> QHash_QString_Macro; + +struct DocUtilities : public Singleton<DocUtilities> +{ +public: + QHash_QString_int cmdHash; + QHash_QString_Macro macroHash; +}; + +QT_END_NAMESPACE + +#endif // DOCUTILITIES_H diff --git a/src/qdoc/qdoc/src/qdoc/editdistance.cpp b/src/qdoc/qdoc/src/qdoc/editdistance.cpp new file mode 100644 index 000000000..303979ec3 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/editdistance.cpp @@ -0,0 +1,68 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "editdistance.h" + +QT_BEGIN_NAMESPACE + +int editDistance(const QString &s, const QString &t) +{ +#define D(i, j) d[(i)*n + (j)] + int i; + int j; + qsizetype m = s.size() + 1; + qsizetype n = t.size() + 1; + int *d = new int[m * n]; + int result; + + for (i = 0; i < m; ++i) + D(i, 0) = i; + for (j = 0; j < n; ++j) + D(0, j) = j; + for (i = 1; i < m; ++i) { + for (j = 1; j < n; ++j) { + if (s[i - 1] == t[j - 1]) { + D(i, j) = D(i - 1, j - 1); + } else { + int x = D(i - 1, j); + int y = D(i - 1, j - 1); + int z = D(i, j - 1); + D(i, j) = 1 + qMin(qMin(x, y), z); + } + } + } + result = D(m - 1, n - 1); + delete[] d; + return result; +#undef D +} + +QString nearestName(const QString &actual, const QSet<QString> &candidates) +{ + if (actual.isEmpty()) + return QString(); + + int deltaBest = 10000; + int numBest = 0; + QString best; + + for (const auto &candidate : candidates) { + if (candidate[0] == actual[0]) { + int delta = editDistance(actual, candidate); + if (delta < deltaBest) { + deltaBest = delta; + numBest = 1; + best = candidate; + } else if (delta == deltaBest) { + ++numBest; + } + } + } + + if (numBest == 1 && deltaBest <= 2 && actual.size() + best.size() >= 5) + return best; + + return QString(); +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/editdistance.h b/src/qdoc/qdoc/src/qdoc/editdistance.h new file mode 100644 index 000000000..dfa6a42dd --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/editdistance.h @@ -0,0 +1,17 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef EDITDISTANCE_H +#define EDITDISTANCE_H + +#include <QtCore/qset.h> +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +int editDistance(const QString &s, const QString &t); +QString nearestName(const QString &actual, const QSet<QString> &candidates); + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/enumitem.h b/src/qdoc/qdoc/src/qdoc/enumitem.h new file mode 100644 index 000000000..06e9d42a9 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/enumitem.h @@ -0,0 +1,36 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef ENUMITEM_H +#define ENUMITEM_H + +#include <QtCore/qglobal.h> +#include <QtCore/qstring.h> + +#include <utility> + +QT_BEGIN_NAMESPACE + +class EnumItem +{ +public: + EnumItem() = default; + EnumItem(QString name, QString value, QString since = QString()) + : m_name(std::move(name)), + m_value(std::move(value)), + m_since(std::move(since)) {} + + [[nodiscard]] const QString &name() const { return m_name; } + [[nodiscard]] const QString &value() const { return m_value; } + [[nodiscard]] const QString &since() const { return m_since; } + void setSince(const QString &since) { m_since = since; } + +private: + QString m_name {}; + QString m_value {}; + QString m_since {}; +}; + +QT_END_NAMESPACE + +#endif // ENUMITEM_H diff --git a/src/qdoc/qdoc/src/qdoc/enumnode.cpp b/src/qdoc/qdoc/src/qdoc/enumnode.cpp new file mode 100644 index 000000000..48a2f81aa --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/enumnode.cpp @@ -0,0 +1,82 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "enumnode.h" + +#include "aggregate.h" +#include "typedefnode.h" + +QT_BEGIN_NAMESPACE + +/*! + \class EnumNode + */ + +/*! + Add \a item to the enum type's item list. + */ +void EnumNode::addItem(const EnumItem &item) +{ + m_items.append(item); + m_names.insert(item.name()); +} + +/*! + Returns the access level of the enumeration item named \a name. + Apparently it is private if it has been omitted by qdoc's + omitvalue command. Otherwise it is public. + */ +Access EnumNode::itemAccess(const QString &name) const +{ + if (doc().omitEnumItemNames().contains(name)) + return Access::Private; + return Access::Public; +} + +/*! + Returns the enum value associated with the enum \a name. + */ +QString EnumNode::itemValue(const QString &name) const +{ + for (const auto &item : std::as_const(m_items)) { + if (item.name() == name) + return item.value(); + } + return QString(); +} + +/*! + Sets \a since information to a named enum \a value, if it + exists in this enum. +*/ +void EnumNode::setSince(const QString &value, const QString &since) +{ + auto it = std::find_if(m_items.begin(), m_items.end(), [value](EnumItem ev) { + return ev.name() == value; + }); + if (it != m_items.end()) + it->setSince(since); +} + +/*! + Clone this node on the heap and make the clone a child of + \a parent. + + Returns a pointer to the clone. + */ +Node *EnumNode::clone(Aggregate *parent) +{ + auto *en = new EnumNode(*this); // shallow copy + en->setParent(nullptr); + parent->addChild(en); + + return en; +} + +void EnumNode::setFlagsType(TypedefNode *typedefNode) +{ + m_flagsType = typedefNode; + typedefNode->setAssociatedEnum(this); +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/enumnode.h b/src/qdoc/qdoc/src/qdoc/enumnode.h new file mode 100644 index 000000000..47139ae4d --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/enumnode.h @@ -0,0 +1,49 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef ENUMNODE_H +#define ENUMNODE_H + +#include "access.h" +#include "node.h" +#include "typedefnode.h" + +#include <QtCore/qglobal.h> +#include <QtCore/qlist.h> +#include <QtCore/qset.h> +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +class Aggregate; + +class EnumNode : public Node +{ +public: + EnumNode(Aggregate *parent, const QString &name, bool isScoped = false) + : Node(Enum, parent, name), m_isScoped(isScoped) + { + } + + void addItem(const EnumItem &item); + void setFlagsType(TypedefNode *typedefNode); + bool hasItem(const QString &name) const { return m_names.contains(name); } + bool isScoped() const { return m_isScoped; } + + const QList<EnumItem> &items() const { return m_items; } + Access itemAccess(const QString &name) const; + const TypedefNode *flagsType() const { return m_flagsType; } + QString itemValue(const QString &name) const; + Node *clone(Aggregate *parent) override; + void setSince(const QString &value, const QString &since); + +private: + QList<EnumItem> m_items {}; + QSet<QString> m_names {}; + const TypedefNode *m_flagsType { nullptr }; + bool m_isScoped { false }; +}; + +QT_END_NAMESPACE + +#endif // ENUMNODE_H diff --git a/src/qdoc/qdoc/src/qdoc/examplenode.h b/src/qdoc/qdoc/src/qdoc/examplenode.h new file mode 100644 index 000000000..920092e4e --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/examplenode.h @@ -0,0 +1,42 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef EXAMPLENODE_H +#define EXAMPLENODE_H + +#include "pagenode.h" + +#include <QtCore/qglobal.h> +#include <QtCore/qstring.h> +#include <QtCore/qstringlist.h> + +QT_BEGIN_NAMESPACE + +class ExampleNode : public PageNode +{ +public: + ExampleNode(Aggregate *parent, const QString &name) : PageNode(Node::Example, parent, name) {} + [[nodiscard]] QString imageFileName() const override { return m_imageFileName; } + void setImageFileName(const QString &ifn) override { m_imageFileName = ifn; } + [[nodiscard]] const QStringList &files() const { return m_files; } + [[nodiscard]] const QStringList &images() const { return m_images; } + [[nodiscard]] const QString &projectFile() const { return m_projectFile; } + void setFiles(const QStringList &files, const QString &projectFile) + { + m_files = files; + m_projectFile = projectFile; + } + void setImages(const QStringList &images) { m_images = images; } + void appendFile(QString &file) { m_files.append(file); } + void appendImage(QString &image) { m_images.append(image); } + +private: + QString m_imageFileName {}; + QString m_projectFile {}; + QStringList m_files {}; + QStringList m_images {}; +}; + +QT_END_NAMESPACE + +#endif // EXAMPLENODE_H diff --git a/src/qdoc/qdoc/src/qdoc/externalpagenode.cpp b/src/qdoc/qdoc/src/qdoc/externalpagenode.cpp new file mode 100644 index 000000000..30a583d00 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/externalpagenode.cpp @@ -0,0 +1,28 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "externalpagenode.h" + +QT_BEGIN_NAMESPACE + +/*! + \class ExternalPageNode + + \brief The ExternalPageNode represents an external documentation page. + + Qdoc can generate links to pages that are not part of the documentation + being generated. 3rd party software pages are often referenced by links + from the QT documentation. Qdoc creates an ExternalPageNode when it sees + an \c {\\externalpage} command. The HTML generator can then use the node + when it needs to create links to the external page. + + ExternalPageNode inherits PageNode. +*/ + +/*! \fn ExternalPageNode::ExternalPageNode(Aggregate *parent, const QString &name) + The constructor creates an ExternalPageNode as a child node of \a parent. + It's \a name is the argument from the \c {\\externalpage} command. The node + type is Node::ExternalPage, and the page type is Node::ArticlePage. + */ + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/externalpagenode.h b/src/qdoc/qdoc/src/qdoc/externalpagenode.h new file mode 100644 index 000000000..e67aab0e8 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/externalpagenode.h @@ -0,0 +1,25 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef EXTERNALPAGENODE_H +#define EXTERNALPAGENODE_H + +#include "pagenode.h" + +#include <QtCore/qglobal.h> + +QT_BEGIN_NAMESPACE + +class ExternalPageNode : public PageNode +{ +public: + ExternalPageNode(Aggregate *parent, const QString &url) + : PageNode(Node::ExternalPage, parent, url) + { + setUrl(url); + } +}; + +QT_END_NAMESPACE + +#endif // EXTERNALPAGENODE_H diff --git a/src/qdoc/qdoc/src/qdoc/filesystem/fileresolver.cpp b/src/qdoc/qdoc/src/qdoc/filesystem/fileresolver.cpp new file mode 100644 index 000000000..aaa489085 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/filesystem/fileresolver.cpp @@ -0,0 +1,161 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "fileresolver.h" + +#include "qdoc/boundaries/filesystem/filepath.h" + +#include <QDir> + +#include <iostream> +#include <algorithm> + +/*! + * \class FileResolver + * \brief Encapsulate the logic that QDoc uses to find files whose + * path is provided by the user and that are relative to the current + * configuration. + * + * A FileResolver instance is configured during creation, defining the + * root directories that the search should be performed on. + * + * Afterwards, it can be used to resolve paths relative to those + * directories, by querying through the resolve() method. + * + * Queries are resolved through a linear search through root + * directories, finding at most one file each time. + * A file is considered to be resolved if, from any root directory, + * the query represents an existing file. + * + * For example, consider the following directory structure on some + * filesystem: + * + * \badcode + * foo/ + * | + * |-bar/ + * |-| + * | |-anotherfile.txt + * |-file.txt + * \endcode + * + * And consider an instance of FileResolver tha considers \e{foo/} to + * be a root directory for search. + * + * Then, queries such as \e {bar/anotherfile.txt} and \e {file.txt} + * will be resolved. + * + * Instead, queries such as \e {foobar.cpp}, \e {bar}, and \e + * {foo/bar/anotherfile.txt} will not be resolved, as they do not + * represent any file reachable from a root directory for search. + * + * It is important to note that FileResolver always searches its root + * directories in an order that is based on the lexicographic ordering + * of the path of its root directories. + * + * For example, consider the following directory structure on some + * filesystem: + * + * \badcode + * foo/ + * | + * |-bar/ + * |-| + * | |-file.txt + * |-foobar/ + * |-| + * | |-file.txt + * \endcode + * + * And consider an instance of FileResolver that considers \e + * {foo/bar/} and \e {foo/foobar/} to be root directories for search. + * + * Then, when the query \e {file.txt} is resolved, it will always + * resolve to the file in \e {bar}, as \e {bar} will be searched + * before \e {foobar}. + * + * We say that \e {foobar/file.txt} is shadowed by \e {bar/file.txt}. + * + * Currently, if this is an issue, it is possible to resolve it by + * using a common ancestor as a root directory instead of using + * multiples directories. + * + * In the previous example, if \e {foo} is instead chosen as the root + * directory for search, then queries \e {bar/file.txt} and \e + * {foobar/file.txt} can be used to uniquely resolve the two files, + * removing the shadowing. + * */ + +/*! + * Constructs an instance of FileResolver with the directories in \a + * search_directories as root directories for searching. + * + * Duplicates in \a search_directories do not affect the resolution of + * files for the instance. + * + * For example, if \a search_directories contains some directory D + * more than once, the constructed instance will resolve files + * equivalently to an instance constructed with a single appearance of + * D. + * + * The order of \a search_directories does not affect the resolution + * of files for an instance. + * + * For example, if \a search_directories contains a permutation of + * directories D1, D2, ..., Dn, then the constructed instance will + * resolve files equivalently to an instance constructed from a + * difference permutation of the same directories. + */ +FileResolver::FileResolver(std::vector<DirectoryPath>&& search_directories) + : search_directories{std::move(search_directories)} +{ + std::sort(this->search_directories.begin(), this->search_directories.end()); + this->search_directories.erase ( + std::unique(this->search_directories.begin(), this->search_directories.end()), + this->search_directories.end() + ); +} + +// REMARK: Note that we do not treat absolute path specially. +// This will in general mean that they cannot get resolved (albeit +// there is a peculiar instance in which they can considering that +// most path formats treat multiple adjacent separators as one). +// +// While we need to treat them at some point with a specific +// intention, this was avoided at the current moment as it is +// unrequired to build the actual documentation. +// +// In particular, avoiding this choice now allows us to move it to a +// later stage where we can work with the origin of the data itself. +// User-inputted paths come into the picture during the configuration +// process and when parsing qdoc comments, there is a good chance that +// some amount of sophistication will be required to handle this data +// at the code level, for example to ensure that multiplatform +// handling of paths is performed correctly. +// +// This will then define how we should handle absolute paths, if we +// can receive them at all and so on. + +/*! +* Returns a ResolvedFile if \a query can be resolved or std::nullopt +* otherwise. +* +* The returned ResolvedFile, if any, will contain the provided \a +* query and the path that the \a query was resolved to. +*/ +[[nodiscard]] std::optional<ResolvedFile> FileResolver::resolve(QString query) const { + for (auto& directory_path : search_directories) { + auto maybe_filepath = FilePath::refine(QDir(directory_path.value() + "/" + query).path()); + if (maybe_filepath) return ResolvedFile{std::move(query), std::move(*maybe_filepath)}; + } + + return std::nullopt; +} + +/*! + * \fn FileResolver::get_search_directories() const + * + * Returns a const-reference to a collection of root search + * directories that this instance will use during the resolution of + * files. + */ diff --git a/src/qdoc/qdoc/src/qdoc/filesystem/fileresolver.h b/src/qdoc/qdoc/src/qdoc/filesystem/fileresolver.h new file mode 100644 index 000000000..be574da30 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/filesystem/fileresolver.h @@ -0,0 +1,24 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "qdoc/boundaries/filesystem/directorypath.h" +#include "qdoc/boundaries/filesystem/resolvedfile.h" + +#include <optional> +#include <vector> + +#include <QtCore/qstring.h> + +class FileResolver { +public: + FileResolver(std::vector<DirectoryPath>&& search_directories); + + [[nodiscard]] std::optional<ResolvedFile> resolve(QString filename) const; + + [[nodiscard]] const std::vector<DirectoryPath>& get_search_directories() const { return search_directories; } + +private: + std::vector<DirectoryPath> search_directories; +}; diff --git a/src/qdoc/qdoc/src/qdoc/functionnode.cpp b/src/qdoc/qdoc/src/qdoc/functionnode.cpp new file mode 100644 index 000000000..82933e0ac --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/functionnode.cpp @@ -0,0 +1,473 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "functionnode.h" +#include "propertynode.h" + +#include <string> + +QT_BEGIN_NAMESPACE + +/*! + \class FunctionNode + + This node is used to represent any kind of function being + documented. It can represent a C++ class member function, a C++ + global function, a QML method, or a macro, with or without + parameters. + + A C++ function can be a signal, a slot, a constructor of any + kind, a destructor, a copy or move assignment operator, or + just a plain old member function or a global function. + + A QML method can be a plain old method, or a + signal or signal handler. + + If the function is an overload, its overload flag is + true. + + The function node also has an overload number. If the + node's overload flag is set, this overload number is + positive; otherwise, the overload number is 0. + */ + +/*! + Construct a function node for a C++ function. It's parent + is \a parent, and it's name is \a name. + + \note The function node's overload flag is set to false, and + its overload number is set to 0. These data members are set + in normalizeOverloads(), when all the overloads are known. + */ +FunctionNode::FunctionNode(Aggregate *parent, const QString &name) + : Node(Function, parent, name), + m_const(false), + m_default(false), + m_static(false), + m_reimpFlag(false), + m_attached(false), + m_overloadFlag(false), + m_isFinal(false), + m_isOverride(false), + m_isRef(false), + m_isRefRef(false), + m_isInvokable(false), + m_explicit{false}, + m_constexpr{false}, + m_metaness(Plain), + m_virtualness(NonVirtual), + m_overloadNumber(0) +{ + // nothing +} + +/*! + Construct a function node for a QML method or signal, specified + by ther Metaness value \a type. It's parent is \a parent, and + it's name is \a name. If \a attached is true, it is an attached + method or signal. + + \note The function node's overload flag is set to false, and + its overload number is set to 0. These data members are set + in normalizeOverloads(), when all the overloads are known. + */ +FunctionNode::FunctionNode(Metaness kind, Aggregate *parent, const QString &name, bool attached) + : Node(Function, parent, name), + m_const(false), + m_default(false), + m_static(false), + m_reimpFlag(false), + m_attached(attached), + m_overloadFlag(false), + m_isFinal(false), + m_isOverride(false), + m_isRef(false), + m_isRefRef(false), + m_isInvokable(false), + m_explicit{false}, + m_constexpr{false}, + m_metaness(kind), + m_virtualness(NonVirtual), + m_overloadNumber(0) +{ + setGenus(getGenus(m_metaness)); + if (!isCppNode() && name.startsWith("__")) + setStatus(Internal); +} + +/*! + Clone this node on the heap and make the clone a child of + \a parent. Return the pointer to the clone. + */ +Node *FunctionNode::clone(Aggregate *parent) +{ + auto *fn = new FunctionNode(*this); // shallow copy + fn->setParent(nullptr); + parent->addChild(fn); + return fn; +} + +/*! + Returns this function's virtualness value as a string + for use as an attribute value in index files. + */ +QString FunctionNode::virtualness() const +{ + switch (m_virtualness) { + case FunctionNode::NormalVirtual: + return QLatin1String("virtual"); + case FunctionNode::PureVirtual: + return QLatin1String("pure"); + case FunctionNode::NonVirtual: + default: + break; + } + return QLatin1String("non"); +} + +/*! + Sets the function node's virtualness value based on the value + of string \a value, which is the value of the function's \e{virtual} + attribute in an index file. If \a value is \e{pure}, and if the + parent() is a C++ class, set the parent's \e abstract flag to + \c {true}. + */ +void FunctionNode::setVirtualness(const QString &value) +{ + if (value == QLatin1String("pure")) { + m_virtualness = PureVirtual; + if (parent() && parent()->isClassNode()) + parent()->setAbstract(true); + return; + } + + m_virtualness = (value == QLatin1String("virtual")) ? NormalVirtual : NonVirtual; +} + +static QMap<QString, FunctionNode::Metaness> metanessMap_; +static void buildMetanessMap() +{ + metanessMap_["plain"] = FunctionNode::Plain; + metanessMap_["signal"] = FunctionNode::Signal; + metanessMap_["slot"] = FunctionNode::Slot; + metanessMap_["constructor"] = FunctionNode::Ctor; + metanessMap_["copy-constructor"] = FunctionNode::CCtor; + metanessMap_["move-constructor"] = FunctionNode::MCtor; + metanessMap_["destructor"] = FunctionNode::Dtor; + metanessMap_["macro"] = FunctionNode::MacroWithParams; + metanessMap_["macrowithparams"] = FunctionNode::MacroWithParams; + metanessMap_["macrowithoutparams"] = FunctionNode::MacroWithoutParams; + metanessMap_["copy-assign"] = FunctionNode::CAssign; + metanessMap_["move-assign"] = FunctionNode::MAssign; + metanessMap_["native"] = FunctionNode::Native; + metanessMap_["qmlsignal"] = FunctionNode::QmlSignal; + metanessMap_["qmlsignalhandler"] = FunctionNode::QmlSignalHandler; + metanessMap_["qmlmethod"] = FunctionNode::QmlMethod; +} + +static QMap<QString, FunctionNode::Metaness> topicMetanessMap_; +static void buildTopicMetanessMap() +{ + topicMetanessMap_["fn"] = FunctionNode::Plain; + topicMetanessMap_["qmlsignal"] = FunctionNode::QmlSignal; + topicMetanessMap_["qmlattachedsignal"] = FunctionNode::QmlSignal; + topicMetanessMap_["qmlmethod"] = FunctionNode::QmlMethod; + topicMetanessMap_["qmlattachedmethod"] = FunctionNode::QmlMethod; +} + +/*! + Determines the Genus value for this FunctionNode given the + Metaness value \a metaness. Returns the Genus value. \a metaness must be + one of the values of Metaness. If not, Node::DontCare is + returned. + */ +Node::Genus FunctionNode::getGenus(FunctionNode::Metaness metaness) +{ + switch (metaness) { + case FunctionNode::Plain: + case FunctionNode::Signal: + case FunctionNode::Slot: + case FunctionNode::Ctor: + case FunctionNode::Dtor: + case FunctionNode::CCtor: + case FunctionNode::MCtor: + case FunctionNode::MacroWithParams: + case FunctionNode::MacroWithoutParams: + case FunctionNode::Native: + case FunctionNode::CAssign: + case FunctionNode::MAssign: + return Node::CPP; + case FunctionNode::QmlSignal: + case FunctionNode::QmlSignalHandler: + case FunctionNode::QmlMethod: + return Node::QML; + } + + return Node::DontCare; +} + +/*! + This static function converts the string \a value to an enum + value for the kind of function named by \a value. + */ +FunctionNode::Metaness FunctionNode::getMetaness(const QString &value) +{ + if (metanessMap_.isEmpty()) + buildMetanessMap(); + return metanessMap_[value]; +} + +/*! + This static function converts the topic string \a topic to an enum + value for the kind of function this FunctionNode represents. + */ +FunctionNode::Metaness FunctionNode::getMetanessFromTopic(const QString &topic) +{ + if (topicMetanessMap_.isEmpty()) + buildTopicMetanessMap(); + return topicMetanessMap_[topic]; +} + +/*! + Sets the function node's overload number to \a number. If \a number + is 0, the function node's overload flag is set to false. If + \a number is greater than 0, the overload flag is set to true. + */ +void FunctionNode::setOverloadNumber(signed short number) +{ + m_overloadNumber = number; + m_overloadFlag = (number > 0); +} + +/*! + \fn void FunctionNode::setReimpFlag() + + Sets the function node's reimp flag to \c true, which means + the \e {\\reimp} command was used in the qdoc comment. It is + supposed to mean that the function reimplements a virtual + function in a base class. + */ + +/*! + Returns a string representing the kind of function this + Function node represents, which depends on the Metaness + value. + */ +QString FunctionNode::kindString() const +{ + switch (m_metaness) { + case FunctionNode::QmlSignal: + return "QML signal"; + case FunctionNode::QmlSignalHandler: + return "QML signal handler"; + case FunctionNode::QmlMethod: + return "QML method"; + default: + return "function"; + } +} + +/*! + Returns a string representing the Metaness enum value for + this function. It is used in index files. + */ +QString FunctionNode::metanessString() const +{ + switch (m_metaness) { + case FunctionNode::Plain: + return "plain"; + case FunctionNode::Signal: + return "signal"; + case FunctionNode::Slot: + return "slot"; + case FunctionNode::Ctor: + return "constructor"; + case FunctionNode::CCtor: + return "copy-constructor"; + case FunctionNode::MCtor: + return "move-constructor"; + case FunctionNode::Dtor: + return "destructor"; + case FunctionNode::MacroWithParams: + return "macrowithparams"; + case FunctionNode::MacroWithoutParams: + return "macrowithoutparams"; + case FunctionNode::Native: + return "native"; + case FunctionNode::CAssign: + return "copy-assign"; + case FunctionNode::MAssign: + return "move-assign"; + case FunctionNode::QmlSignal: + return "qmlsignal"; + case FunctionNode::QmlSignalHandler: + return "qmlsignalhandler"; + case FunctionNode::QmlMethod: + return "qmlmethod"; + default: + return "plain"; + } +} + +/*! + Adds the "associated" property \a p to this function node. + The function might be the setter or getter for a property, + for example. + */ +void FunctionNode::addAssociatedProperty(PropertyNode *p) +{ + m_associatedProperties.append(p); +} + +/*! + \reimp + + Returns \c true if this is an access function for an obsolete property, + otherwise calls the base implementation of isDeprecated(). +*/ +bool FunctionNode::isDeprecated() const +{ + auto it = std::find_if_not(m_associatedProperties.begin(), m_associatedProperties.end(), + [](const Node *p) -> bool { return p->isDeprecated(); }); + + if (!m_associatedProperties.isEmpty() && it == m_associatedProperties.end()) + return true; + + return Node::isDeprecated(); +} + +/*! \fn unsigned char FunctionNode::overloadNumber() const + Returns the overload number for this function. + */ + +/*! + Reconstructs and returns the function's signature. + + Specific parts of the signature are included according to + flags in \a options: + + \value Node::SignaturePlain + Plain signature + \value Node::SignatureDefaultValues + Include any default argument values + \value Node::SignatureReturnType + Include return type + \value Node::SignatureTemplateParams + Include \c {template <parameter_list>} if one exists + */ +QString FunctionNode::signature(Node::SignatureOptions options) const +{ + QStringList elements; + + if (options & Node::SignatureTemplateParams && templateDecl()) + elements << (*templateDecl()).to_qstring(); + if (options & Node::SignatureReturnType) + elements << m_returnType; + elements.removeAll(QString()); + + if (!isMacroWithoutParams()) { + elements << name() + QLatin1Char('(') + + m_parameters.signature(options & Node::SignatureDefaultValues) + + QLatin1Char(')'); + if (!isMacro()) { + if (isConst()) + elements << QStringLiteral("const"); + if (isRef()) + elements << QStringLiteral("&"); + else if (isRefRef()) + elements << QStringLiteral("&&"); + } + } else { + elements << name(); + } + return elements.join(QLatin1Char(' ')); +} + +/*! + \fn int FunctionNode::compare(const FunctionNode *f1, const FunctionNode *f2) + + Compares FunctionNode \a f1 with \a f2, assumed to have identical names. + Returns an integer less than, equal to, or greater than zero if f1 is + considered less than, equal to, or greater than f2. + + The main purpose is to provide stable ordering for function overloads. + */ +[[nodiscard]] int compare(const FunctionNode *f1, const FunctionNode *f2) +{ + // Compare parameter count + int param_count{f1->parameters().count()}; + + if (int param_diff = param_count - f2->parameters().count(); param_diff != 0) + return param_diff; + + // Constness + if (f1->isConst() != f2->isConst()) + return f1->isConst() ? 1 : -1; + + // Reference qualifiers + if (f1->isRef() != f2->isRef()) + return f1->isRef() ? 1 : -1; + if (f1->isRefRef() != f2->isRefRef()) + return f1->isRefRef() ? 1 : -1; + + // Attachedness (applies to QML methods) + if (f1->isAttached() != f2->isAttached()) + return f1->isAttached() ? 1 : -1; + + // Parameter types + const Parameters &p1{f1->parameters()}; + const Parameters &p2{f2->parameters()}; + for (qsizetype i = 0; i < param_count; ++i) { + if (int type_comp = QString::compare(p1.at(i).type(), p2.at(i).type()); + type_comp != 0) { + return type_comp; + } + } + + // Template declarations + const auto t1{f1->templateDecl()}; + const auto t2{f2->templateDecl()}; + if (!t1 && !t2) + return 0; + + if (t1 && t2) + return (*t1).to_std_string().compare((*t2).to_std_string()); + + return t1 ? 1 : -1; +} + +/*! + In some cases, it is ok for a public function to be not documented. + For example, the macro Q_OBJECT adds several functions to the API of + a class, but these functions are normally not meant to be documented. + So if a function node doesn't have documentation, then if its name is + in the list of functions that it is ok not to document, this function + returns true. Otherwise, it returns false. + + These are the member function names added by macros. Usually they + are not documented, but they can be documented, so this test avoids + reporting an error if they are not documented. + + But maybe we should generate a standard text for each of them? + */ +bool FunctionNode::isIgnored() const +{ + if (!hasDoc()) { + if (name().startsWith(QLatin1String("qt_")) || name() == QLatin1String("metaObject") + || name() == QLatin1String("tr") || name() == QLatin1String("trUtf8") + || name() == QLatin1String("d_func")) { + return true; + } + QString s = signature(Node::SignatureReturnType); + if (s.contains(QLatin1String("enum_type")) && s.contains(QLatin1String("operator|"))) + return true; + } + return false; +} + +/*! + \fn bool FunctionNode::hasOverloads() const + Returns \c true if this function has overloads. + */ + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/functionnode.h b/src/qdoc/qdoc/src/qdoc/functionnode.h new file mode 100644 index 000000000..dca8c7e44 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/functionnode.h @@ -0,0 +1,202 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef FUNCTIONNODE_H +#define FUNCTIONNODE_H + +#include "aggregate.h" +#include "node.h" +#include "parameters.h" + +#include <QtCore/qglobal.h> +#include <QtCore/qstring.h> + +#include <optional> + +QT_BEGIN_NAMESPACE + +class FunctionNode : public Node +{ +public: + enum Virtualness { NonVirtual, NormalVirtual, PureVirtual }; + + enum Metaness { + Plain, + Signal, + Slot, + Ctor, + Dtor, + CCtor, // copy constructor + MCtor, // move-copy constructor + MacroWithParams, + MacroWithoutParams, + Native, + CAssign, // copy-assignment operator + MAssign, // move-assignment operator + QmlSignal, + QmlSignalHandler, + QmlMethod, + }; + + FunctionNode(Aggregate *parent, const QString &name); // C++ function (Plain) + FunctionNode(Metaness type, Aggregate *parent, const QString &name, bool attached = false); + + Node *clone(Aggregate *parent) override; + [[nodiscard]] Metaness metaness() const { return m_metaness; } + [[nodiscard]] QString metanessString() const; + void setMetaness(Metaness metaness) { m_metaness = metaness; } + [[nodiscard]] QString kindString() const; + static Metaness getMetaness(const QString &value); + static Metaness getMetanessFromTopic(const QString &topic); + static Genus getGenus(Metaness metaness); + + void setReturnType(const QString &type) { m_returnType = type; } + void setVirtualness(const QString &value); + void setVirtualness(Virtualness virtualness) { m_virtualness = virtualness; } + void setConst(bool b) { m_const = b; } + void setDefault(bool b) { m_default = b; } + void setStatic(bool b) { m_static = b; } + void setReimpFlag() { m_reimpFlag = true; } + void setOverridesThis(const QString &path) { m_overridesThis = path; } + + [[nodiscard]] const QString &returnType() const { return m_returnType; } + [[nodiscard]] QString virtualness() const; + [[nodiscard]] bool isConst() const { return m_const; } + [[nodiscard]] bool isDefault() const override { return m_default; } + [[nodiscard]] bool isStatic() const override { return m_static; } + [[nodiscard]] bool isOverload() const { return m_overloadFlag; } + [[nodiscard]] bool isMarkedReimp() const override { return m_reimpFlag; } + [[nodiscard]] bool isSomeCtor() const { return isCtor() || isCCtor() || isMCtor(); } + [[nodiscard]] bool isMacroWithParams() const { return (m_metaness == MacroWithParams); } + [[nodiscard]] bool isMacroWithoutParams() const { return (m_metaness == MacroWithoutParams); } + [[nodiscard]] bool isMacro() const override + { + return (isMacroWithParams() || isMacroWithoutParams()); + } + [[nodiscard]] bool isDeprecated() const override; + + void markExplicit() { m_explicit = true; } + bool isExplicit() const { return m_explicit; } + + void markConstexpr() { m_constexpr = true; } + bool isConstexpr() const { return m_constexpr; } + + void markNoexcept(QString expression = "") { m_noexcept = expression; } + const std::optional<QString>& getNoexcept() const { return m_noexcept; } + + [[nodiscard]] bool isCppFunction() const { return m_metaness == Plain; } // Is this correct? + [[nodiscard]] bool isSignal() const { return (m_metaness == Signal); } + [[nodiscard]] bool isSlot() const { return (m_metaness == Slot); } + [[nodiscard]] bool isCtor() const { return (m_metaness == Ctor); } + [[nodiscard]] bool isDtor() const { return (m_metaness == Dtor); } + [[nodiscard]] bool isCCtor() const { return (m_metaness == CCtor); } + [[nodiscard]] bool isMCtor() const { return (m_metaness == MCtor); } + [[nodiscard]] bool isCAssign() const { return (m_metaness == CAssign); } + [[nodiscard]] bool isMAssign() const { return (m_metaness == MAssign); } + + [[nodiscard]] bool isQmlMethod() const { return (m_metaness == QmlMethod); } + [[nodiscard]] bool isQmlSignal() const { return (m_metaness == QmlSignal); } + [[nodiscard]] bool isQmlSignalHandler() const { return (m_metaness == QmlSignalHandler); } + + [[nodiscard]] bool isSpecialMemberFunction() const + { + return (isCtor() || isDtor() || isCCtor() || isMCtor() || isCAssign() || isMAssign()); + } + [[nodiscard]] bool isNonvirtual() const { return (m_virtualness == NonVirtual); } + [[nodiscard]] bool isVirtual() const { return (m_virtualness == NormalVirtual); } + [[nodiscard]] bool isPureVirtual() const { return (m_virtualness == PureVirtual); } + [[nodiscard]] bool returnsBool() const { return (m_returnType == QLatin1String("bool")); } + + Parameters ¶meters() { return m_parameters; } + [[nodiscard]] const Parameters ¶meters() const { return m_parameters; } + [[nodiscard]] bool isPrivateSignal() const { return m_parameters.isPrivateSignal(); } + void setParameters(const QString &signature) { m_parameters.set(signature); } + [[nodiscard]] QString signature(Node::SignatureOptions options) const override; + + [[nodiscard]] const QString &overridesThis() const { return m_overridesThis; } + [[nodiscard]] const QList<PropertyNode *> &associatedProperties() const { return m_associatedProperties; } + [[nodiscard]] bool hasAssociatedProperties() const { return !m_associatedProperties.isEmpty(); } + [[nodiscard]] bool hasOneAssociatedProperty() const + { + return (m_associatedProperties.size() == 1); + } + [[nodiscard]] QString element() const override { return parent()->name(); } + [[nodiscard]] bool isAttached() const override { return m_attached; } + [[nodiscard]] QString qmlTypeName() const override { return parent()->qmlTypeName(); } + [[nodiscard]] QString logicalModuleName() const override + { + return parent()->logicalModuleName(); + } + [[nodiscard]] QString logicalModuleVersion() const override + { + return parent()->logicalModuleVersion(); + } + [[nodiscard]] QString logicalModuleIdentifier() const override + { + return parent()->logicalModuleIdentifier(); + } + + void setFinal(bool b) { m_isFinal = b; } + [[nodiscard]] bool isFinal() const { return m_isFinal; } + + void setOverride(bool b) { m_isOverride = b; } + [[nodiscard]] bool isOverride() const { return m_isOverride; } + + void setRef(bool b) { m_isRef = b; } + [[nodiscard]] bool isRef() const { return m_isRef; } + + void setRefRef(bool b) { m_isRefRef = b; } + [[nodiscard]] bool isRefRef() const { return m_isRefRef; } + + void setInvokable(bool b) { m_isInvokable = b; } + [[nodiscard]] bool isInvokable() const { return m_isInvokable; } + + [[nodiscard]] bool hasTag(const QString &tag) const override { return (m_tag == tag); } + void setTag(const QString &tag) { m_tag = tag; } + [[nodiscard]] const QString &tag() const { return m_tag; } + [[nodiscard]] bool isIgnored() const; + [[nodiscard]] bool hasOverloads() const + { + return (m_overloadFlag || (parent() && parent()->hasOverloads(this))); + } + void setOverloadFlag() { m_overloadFlag = true; } + void setOverloadNumber(signed short number); + [[nodiscard]] signed short overloadNumber() const { return m_overloadNumber; } + + friend int compare(const FunctionNode *f1, const FunctionNode *f2); + +private: + void addAssociatedProperty(PropertyNode *property); + + friend class Aggregate; + friend class PropertyNode; + + bool m_const : 1; + bool m_default : 1; + bool m_static : 1; + bool m_reimpFlag : 1; + bool m_attached : 1; + bool m_overloadFlag : 1; + bool m_isFinal : 1; + bool m_isOverride : 1; + bool m_isRef : 1; + bool m_isRefRef : 1; + bool m_isInvokable : 1; + bool m_explicit; + bool m_constexpr; + + std::optional<QString> m_noexcept; + + Metaness m_metaness {}; + Virtualness m_virtualness{ NonVirtual }; + signed short m_overloadNumber {}; + QString m_returnType {}; + QString m_overridesThis {}; + QString m_tag {}; + QList<PropertyNode *> m_associatedProperties {}; + Parameters m_parameters {}; +}; + +QT_END_NAMESPACE + +#endif // FUNCTIONNODE_H diff --git a/src/qdoc/qdoc/src/qdoc/generator.cpp b/src/qdoc/qdoc/src/qdoc/generator.cpp new file mode 100644 index 000000000..d1b3642c3 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/generator.cpp @@ -0,0 +1,2216 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "generator.h" + +#include "access.h" +#include "aggregate.h" +#include "classnode.h" +#include "codemarker.h" +#include "collectionnode.h" +#include "comparisoncategory.h" +#include "config.h" +#include "doc.h" +#include "editdistance.h" +#include "enumnode.h" +#include "examplenode.h" +#include "functionnode.h" +#include "node.h" +#include "openedlist.h" +#include "propertynode.h" +#include "qdocdatabase.h" +#include "qmltypenode.h" +#include "qmlpropertynode.h" +#include "quoter.h" +#include "sharedcommentnode.h" +#include "tokenizer.h" +#include "typedefnode.h" +#include "utilities.h" + +#include <QtCore/qdebug.h> +#include <QtCore/qdir.h> +#include <QtCore/qregularexpression.h> + +#ifndef QT_BOOTSTRAPPED +# include "QtCore/qurl.h" +#endif + +#include <string> + +using namespace std::literals::string_literals; + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +Generator *Generator::s_currentGenerator; +QMap<QString, QMap<QString, QString>> Generator::s_fmtLeftMaps; +QMap<QString, QMap<QString, QString>> Generator::s_fmtRightMaps; +QList<Generator *> Generator::s_generators; +QString Generator::s_outDir; +QString Generator::s_outSubdir; +QStringList Generator::s_outFileNames; +QSet<QString> Generator::s_trademarks; +QSet<QString> Generator::s_outputFormats; +QHash<QString, QString> Generator::s_outputPrefixes; +QHash<QString, QString> Generator::s_outputSuffixes; +QString Generator::s_project; +bool Generator::s_noLinkErrors = false; +bool Generator::s_autolinkErrors = false; +bool Generator::s_redirectDocumentationToDevNull = false; +bool Generator::s_useOutputSubdirs = true; +QmlTypeNode *Generator::s_qmlTypeContext = nullptr; + +static QRegularExpression tag("</?@[^>]*>"); +static QLatin1String amp("&"); +static QLatin1String gt(">"); +static QLatin1String lt("<"); +static QLatin1String quot("""); + +/*! + Constructs the generator base class. Prepends the newly + constructed generator to the list of output generators. + Sets a pointer to the QDoc database singleton, which is + available to the generator subclasses. + */ +Generator::Generator(FileResolver& file_resolver) + : file_resolver{file_resolver} +{ + m_qdb = QDocDatabase::qdocDB(); + s_generators.prepend(this); +} + +/*! + Destroys the generator after removing it from the list of + output generators. + */ +Generator::~Generator() +{ + s_generators.removeAll(this); +} + +void Generator::appendFullName(Text &text, const Node *apparentNode, const Node *relative, + const Node *actualNode) +{ + if (actualNode == nullptr) + actualNode = apparentNode; + text << Atom(Atom::LinkNode, CodeMarker::stringForNode(actualNode)) + << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) + << Atom(Atom::String, apparentNode->plainFullName(relative)) + << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); +} + +void Generator::appendFullName(Text &text, const Node *apparentNode, const QString &fullName, + const Node *actualNode) +{ + if (actualNode == nullptr) + actualNode = apparentNode; + text << Atom(Atom::LinkNode, CodeMarker::stringForNode(actualNode)) + << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << Atom(Atom::String, fullName) + << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); +} + +/*! + Append the signature for the function named in \a node to + \a text, so that is a link to the documentation for that + function. + */ +void Generator::appendSignature(Text &text, const Node *node) +{ + text << Atom(Atom::LinkNode, CodeMarker::stringForNode(node)) + << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) + << Atom(Atom::String, node->signature(Node::SignaturePlain)) + << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); +} + +/*! + Generate a bullet list of function signatures. The function + nodes are in \a nodes. It uses the \a relative node and the + \a marker for the generation. + */ +void Generator::signatureList(const NodeList &nodes, const Node *relative, CodeMarker *marker) +{ + Text text; + int count = 0; + text << Atom(Atom::ListLeft, QString("bullet")); + for (const auto &node : nodes) { + text << Atom(Atom::ListItemNumber, QString::number(++count)); + text << Atom(Atom::ListItemLeft, QString("bullet")); + appendSignature(text, node); + text << Atom(Atom::ListItemRight, QString("bullet")); + } + text << Atom(Atom::ListRight, QString("bullet")); + generateText(text, relative, marker); +} + +int Generator::appendSortedNames(Text &text, const ClassNode *cn, const QList<RelatedClass> &rc) +{ + QMap<QString, Text> classMap; + for (const auto &relatedClass : rc) { + ClassNode *rcn = relatedClass.m_node; + if (rcn && rcn->isInAPI()) { + Text className; + appendFullName(className, rcn, cn); + classMap[className.toString().toLower()] = className; + } + } + + int index = 0; + const QStringList classNames = classMap.keys(); + for (const auto &className : classNames) { + text << classMap[className]; + text << Utilities::comma(index++, classNames.size()); + } + return index; +} + +int Generator::appendSortedQmlNames(Text &text, const Node *base, const NodeList &subs) +{ + QMap<QString, Text> classMap; + + for (const auto sub : subs) { + Text full_name; + appendFullName(full_name, sub, base); + classMap[full_name.toString().toLower()] = full_name; + } + + int index = 0; + const auto &names = classMap.keys(); + for (const auto &name : names) + text << classMap[name] << Utilities::comma(index++, names.size()); + return index; +} + +/*! + Creates the file named \a fileName in the output directory + and returns a QFile pointing to this file. In particular, + this method deals with errors when opening the file: + the returned QFile is always valid and can be written to. + + \sa beginSubPage() + */ +QFile *Generator::openSubPageFile(const Node *node, const QString &fileName) +{ + if (s_outFileNames.contains(fileName)) + node->location().warning("Already generated %1 for this project"_L1.arg(fileName)); + + QString path = outputDir() + QLatin1Char('/') + fileName; + + auto outPath = s_redirectDocumentationToDevNull ? QStringLiteral("/dev/null") : path; + auto outFile = new QFile(outPath); + + if (!s_redirectDocumentationToDevNull && outFile->exists()) { + const QString warningText {"Output file already exists, overwriting %1"_L1.arg(outFile->fileName())}; + if (qEnvironmentVariableIsSet("QDOC_ALL_OVERWRITES_ARE_WARNINGS")) + node->location().warning(warningText); + else + qCDebug(lcQdoc) << qUtf8Printable(warningText); + } + + if (!outFile->open(QFile::WriteOnly | QFile::Text)) { + node->location().fatal( + QStringLiteral("Cannot open output file '%1'").arg(outFile->fileName())); + } + + qCDebug(lcQdoc, "Writing: %s", qPrintable(path)); + s_outFileNames << fileName; + s_trademarks.clear(); + return outFile; +} + +/*! + Creates the file named \a fileName in the output directory. + Attaches a QTextStream to the created file, which is written + to all over the place using out(). + */ +void Generator::beginSubPage(const Node *node, const QString &fileName) +{ + QFile *outFile = openSubPageFile(node, fileName); + auto *out = new QTextStream(outFile); + outStreamStack.push(out); +} + +/*! + Flush the text stream associated with the subpage, and + then pop it off the text stream stack and delete it. + This terminates output of the subpage. + */ +void Generator::endSubPage() +{ + outStreamStack.top()->flush(); + delete outStreamStack.top()->device(); + delete outStreamStack.pop(); +} + +QString Generator::fileBase(const Node *node) const +{ + if (!node->isPageNode() && !node->isCollectionNode()) + node = node->parent(); + + if (node->hasFileNameBase()) + return node->fileNameBase(); + + QString base{node->name()}; + if (base.endsWith(".html")) + base.truncate(base.size() - 5); + + if (node->isCollectionNode()) { + if (node->isQmlModule()) + base.append("-qmlmodule"); + else if (node->isModule()) + base.append("-module"); + base.append(outputSuffix(node)); + } else if (node->isTextPageNode()) { + if (node->isExample()) { + base.prepend("%1-"_L1.arg(s_project.toLower())); + base.append("-example"); + } + } else if (node->isQmlType()) { + /* + To avoid file name conflicts in the html directory, + we prepend a prefix (by default, "qml-") and an optional suffix + to the file name. The suffix, if one exists, is appended to the + module name. + + For historical reasons, skip the module name qualifier for QML value types + in order to avoid excess redirects in the online docs. TODO: re-assess + */ + if (!node->logicalModuleName().isEmpty() && !node->isQmlBasicType() + && (!node->logicalModule()->isInternal() || m_showInternal)) + base.prepend("%1%2-"_L1.arg(node->logicalModuleName(), outputSuffix(node))); + + } else if (node->isProxyNode()) { + base.append("-%1-proxy"_L1.arg(node->tree()->physicalModuleName())); + } else { + base.clear(); + const Node *p = node; + forever { + const Node *pp = p->parent(); + base.prepend(p->name()); + if (pp == nullptr || pp->name().isEmpty() || pp->isTextPageNode()) + break; + base.prepend('-'_L1); + p = pp; + } + if (node->isNamespace() && !node->name().isEmpty()) { + const auto *ns = static_cast<const NamespaceNode *>(node); + if (!ns->isDocumentedHere()) { + base.append(QLatin1String("-sub-")); + base.append(ns->tree()->camelCaseModuleName()); + } + } + base.append(outputSuffix(node)); + } + + base.prepend(outputPrefix(node)); + QString canonicalName{ Utilities::asAsciiPrintable(base) }; + Node *n = const_cast<Node *>(node); + n->setFileNameBase(canonicalName); + return canonicalName; +} + +/*! + Constructs an href link from an example file name, which + is a \a path to the example file. If \a fileExt is empty + (default value), retrieve the file extension from + the generator. + */ +QString Generator::linkForExampleFile(const QString &path, const QString &fileExt) +{ + QString link{path}; + link.prepend(s_project.toLower() + QLatin1Char('-')); + + QString canonicalName{ Utilities::asAsciiPrintable(link) }; + canonicalName.append(QLatin1Char('.')); + canonicalName.append(fileExt.isEmpty() ? fileExtension() : fileExt); + return canonicalName; +} + +/*! + Helper function to construct a title for a file or image page + included in an example. +*/ +QString Generator::exampleFileTitle(const ExampleNode *relative, const QString &fileName) +{ + QString suffix; + if (relative->files().contains(fileName)) + suffix = QLatin1String(" Example File"); + else if (relative->images().contains(fileName)) + suffix = QLatin1String(" Image File"); + else + return suffix; + + return fileName.mid(fileName.lastIndexOf(QLatin1Char('/')) + 1) + suffix; +} + +/*! + If the \a node has a URL, return the URL as the file name. + Otherwise, construct the file name from the fileBase() and + either the provided \a extension or fileExtension(), and + return the constructed name. + */ +QString Generator::fileName(const Node *node, const QString &extension) const +{ + if (!node->url().isEmpty()) + return node->url(); + + QString name = fileBase(node) + QLatin1Char('.'); + return name + (extension.isNull() ? fileExtension() : extension); +} + +/*! + Clean the given \a ref to be used as an HTML anchor or an \c xml:id. + If \a xmlCompliant is set to \c true, a stricter process is used, as XML + is more rigorous in what it accepts. Otherwise, if \a xmlCompliant is set to + \c false, the basic HTML transformations are applied. + + More specifically, only XML NCNames are allowed + (https://www.w3.org/TR/REC-xml-names/#NT-NCName). + */ +QString Generator::cleanRef(const QString &ref, bool xmlCompliant) +{ + // XML-compliance is ensured in two ways: + // - no digit (0-9) at the beginning of an ID (many IDs do not respect this property) + // - no colon (:) anywhere in the ID (occurs very rarely) + + QString clean; + + if (ref.isEmpty()) + return clean; + + clean.reserve(ref.size() + 20); + const QChar c = ref[0]; + const uint u = c.unicode(); + + if ((u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z') || (!xmlCompliant && u >= '0' && u <= '9')) { + clean += c; + } else if (xmlCompliant && u >= '0' && u <= '9') { + clean += QLatin1Char('A') + c; + } else if (u == '~') { + clean += "dtor."; + } else if (u == '_') { + clean += "underscore."; + } else { + clean += QLatin1Char('A'); + } + + for (int i = 1; i < ref.size(); i++) { + const QChar c = ref[i]; + const uint u = c.unicode(); + if ((u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z') || (u >= '0' && u <= '9') || u == '-' + || u == '_' || (xmlCompliant && u == ':') || u == '.') { + clean += c; + } else if (c.isSpace()) { + clean += QLatin1Char('-'); + } else if (u == '!') { + clean += "-not"; + } else if (u == '&') { + clean += "-and"; + } else if (u == '<') { + clean += "-lt"; + } else if (u == '=') { + clean += "-eq"; + } else if (u == '>') { + clean += "-gt"; + } else if (u == '#') { + clean += QLatin1Char('#'); + } else { + clean += QLatin1Char('-'); + clean += QString::number(static_cast<int>(u), 16); + } + } + return clean; +} + +QMap<QString, QString> &Generator::formattingLeftMap() +{ + return s_fmtLeftMaps[format()]; +} + +QMap<QString, QString> &Generator::formattingRightMap() +{ + return s_fmtRightMaps[format()]; +} + +/*! + Returns the full document location. + */ +QString Generator::fullDocumentLocation(const Node *node) +{ + if (node == nullptr) + return QString(); + if (!node->url().isEmpty()) + return node->url(); + + QString parentName; + QString anchorRef; + + if (node->isNamespace()) { + /* + The root namespace has no name - check for this before creating + an attribute containing the location of any documentation. + */ + if (!fileBase(node).isEmpty()) + parentName = fileBase(node) + QLatin1Char('.') + currentGenerator()->fileExtension(); + else + return QString(); + } else if (node->isQmlType()) { + return fileBase(node) + QLatin1Char('.') + currentGenerator()->fileExtension(); + } else if (node->isTextPageNode() || node->isCollectionNode()) { + parentName = fileBase(node) + QLatin1Char('.') + currentGenerator()->fileExtension(); + } else if (fileBase(node).isEmpty()) + return QString(); + + Node *parentNode = nullptr; + + if ((parentNode = node->parent())) { + // use the parent's name unless the parent is the root namespace + if (!node->parent()->isNamespace() || !node->parent()->name().isEmpty()) + parentName = fullDocumentLocation(node->parent()); + } + + switch (node->nodeType()) { + case Node::Class: + case Node::Struct: + case Node::Union: + case Node::Namespace: + case Node::Proxy: + parentName = fileBase(node) + QLatin1Char('.') + currentGenerator()->fileExtension(); + break; + case Node::Function: { + const auto *fn = static_cast<const FunctionNode *>(node); + switch (fn->metaness()) { + case FunctionNode::QmlSignal: + anchorRef = QLatin1Char('#') + node->name() + "-signal"; + break; + case FunctionNode::QmlSignalHandler: + anchorRef = QLatin1Char('#') + node->name() + "-signal-handler"; + break; + case FunctionNode::QmlMethod: + anchorRef = QLatin1Char('#') + node->name() + "-method"; + break; + default: + if (fn->isDtor()) + anchorRef = "#dtor." + fn->name().mid(1); + else if (fn->hasOneAssociatedProperty() && fn->doc().isEmpty()) + return fullDocumentLocation(fn->associatedProperties()[0]); + else if (fn->overloadNumber() > 0) + anchorRef = QLatin1Char('#') + cleanRef(fn->name()) + QLatin1Char('-') + + QString::number(fn->overloadNumber()); + else + anchorRef = QLatin1Char('#') + cleanRef(fn->name()); + break; + } + break; + } + /* + Use node->name() instead of fileBase(node) as + the latter returns the name in lower-case. For + HTML anchors, we need to preserve the case. + */ + case Node::Enum: + anchorRef = QLatin1Char('#') + node->name() + "-enum"; + break; + case Node::Typedef: { + const auto *tdef = static_cast<const TypedefNode *>(node); + if (tdef->associatedEnum()) + return fullDocumentLocation(tdef->associatedEnum()); + } Q_FALLTHROUGH(); + case Node::TypeAlias: + anchorRef = QLatin1Char('#') + node->name() + "-typedef"; + break; + case Node::Property: + anchorRef = QLatin1Char('#') + node->name() + "-prop"; + break; + case Node::SharedComment: { + if (!node->isPropertyGroup()) + break; + } Q_FALLTHROUGH(); + case Node::QmlProperty: + if (node->isAttached()) + anchorRef = QLatin1Char('#') + node->name() + "-attached-prop"; + else + anchorRef = QLatin1Char('#') + node->name() + "-prop"; + break; + case Node::Variable: + anchorRef = QLatin1Char('#') + node->name() + "-var"; + break; + case Node::QmlType: + case Node::Page: + case Node::Group: + case Node::HeaderFile: + case Node::Module: + case Node::QmlModule: { + parentName = fileBase(node); + parentName.replace(QLatin1Char('/'), QLatin1Char('-')) + .replace(QLatin1Char('.'), QLatin1Char('-')); + parentName += QLatin1Char('.') + currentGenerator()->fileExtension(); + } break; + default: + break; + } + + if (!node->isClassNode() && !node->isNamespace()) { + if (node->isDeprecated()) + parentName.replace(QLatin1Char('.') + currentGenerator()->fileExtension(), + "-obsolete." + currentGenerator()->fileExtension()); + } + + return parentName.toLower() + anchorRef; +} + +void Generator::generateAlsoList(const Node *node, CodeMarker *marker) +{ + QList<Text> alsoList = node->doc().alsoList(); + supplementAlsoList(node, alsoList); + + if (!alsoList.isEmpty()) { + Text text; + text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) << "See also " + << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD); + + for (int i = 0; i < alsoList.size(); ++i) + text << alsoList.at(i) << Utilities::separator(i, alsoList.size()); + + text << Atom::ParaRight; + generateText(text, node, marker); + } +} + +const Atom *Generator::generateAtomList(const Atom *atom, const Node *relative, CodeMarker *marker, + bool generate, int &numAtoms) +{ + while (atom != nullptr) { + if (atom->type() == Atom::FormatIf) { + int numAtoms0 = numAtoms; + bool rightFormat = canHandleFormat(atom->string()); + atom = generateAtomList(atom->next(), relative, marker, generate && rightFormat, + numAtoms); + if (atom == nullptr) + return nullptr; + + if (atom->type() == Atom::FormatElse) { + ++numAtoms; + atom = generateAtomList(atom->next(), relative, marker, generate && !rightFormat, + numAtoms); + if (atom == nullptr) + return nullptr; + } + + if (atom->type() == Atom::FormatEndif) { + if (generate && numAtoms0 == numAtoms) { + relative->location().warning(QStringLiteral("Output format %1 not handled %2") + .arg(format(), outFileName())); + Atom unhandledFormatAtom(Atom::UnhandledFormat, format()); + generateAtomList(&unhandledFormatAtom, relative, marker, generate, numAtoms); + } + atom = atom->next(); + } + } else if (atom->type() == Atom::FormatElse || atom->type() == Atom::FormatEndif) { + return atom; + } else { + int n = 1; + if (generate) { + n += generateAtom(atom, relative, marker); + numAtoms += n; + } + while (n-- > 0) + atom = atom->next(); + } + } + return nullptr; +} + +/*! + Generate the body of the documentation from the qdoc comment + found with the entity represented by the \a node. + */ +void Generator::generateBody(const Node *node, CodeMarker *marker) +{ + const FunctionNode *fn = node->isFunction() ? static_cast<const FunctionNode *>(node) : nullptr; + if (!node->hasDoc()) { + /* + Test for special function, like a destructor or copy constructor, + that has no documentation. + */ + if (fn) { + if (fn->isDtor()) { + Text text; + text << "Destroys the instance of "; + text << fn->parent()->name() << "."; + if (fn->isVirtual()) + text << " The destructor is virtual."; + out() << "<p>"; + generateText(text, node, marker); + out() << "</p>"; + } else if (fn->isCtor()) { + Text text; + text << "Default constructs an instance of "; + text << fn->parent()->name() << "."; + out() << "<p>"; + generateText(text, node, marker); + out() << "</p>"; + } else if (fn->isCCtor()) { + Text text; + text << "Copy constructor."; + out() << "<p>"; + generateText(text, node, marker); + out() << "</p>"; + } else if (fn->isMCtor()) { + Text text; + text << "Move-copy constructor."; + out() << "<p>"; + generateText(text, node, marker); + out() << "</p>"; + } else if (fn->isCAssign()) { + Text text; + text << "Copy-assignment operator."; + out() << "<p>"; + generateText(text, node, marker); + out() << "</p>"; + } else if (fn->isMAssign()) { + Text text; + text << "Move-assignment operator."; + out() << "<p>"; + generateText(text, node, marker); + out() << "</p>"; + } else if (!node->isWrapper() && !node->isMarkedReimp()) { + if (!fn->isIgnored()) // undocumented functions added by Q_OBJECT + node->location().warning(QStringLiteral("No documentation for '%1'") + .arg(node->plainSignature())); + } + } else if (!node->isWrapper() && !node->isMarkedReimp()) { + // Don't require documentation of things defined in Q_GADGET + if (node->name() != QLatin1String("QtGadgetHelper")) + node->location().warning( + QStringLiteral("No documentation for '%1'").arg(node->plainSignature())); + } + } else if (!node->isSharingComment()) { + // Reimplements clause and type alias info precede body text + if (fn && !fn->overridesThis().isEmpty()) + generateReimplementsClause(fn, marker); + else if (node->isProperty()) { + if (static_cast<const PropertyNode *>(node)->propertyType() != PropertyNode::PropertyType::StandardProperty) + generateAddendum(node, BindableProperty, marker); + } + + if (!generateText(node->doc().body(), node, marker)) { + if (node->isMarkedReimp()) + return; + } + + if (fn) { + if (fn->isQmlSignal()) + generateAddendum(node, QmlSignalHandler, marker); + if (fn->isPrivateSignal()) + generateAddendum(node, PrivateSignal, marker); + if (fn->isInvokable()) + generateAddendum(node, Invokable, marker); + if (fn->hasAssociatedProperties()) + generateAddendum(node, AssociatedProperties, marker); + } + + // Generate warnings + if (node->isEnumType()) { + const auto *enume = static_cast<const EnumNode *>(node); + + QSet<QString> definedItems; + const QList<EnumItem> &items = enume->items(); + for (const auto &item : items) + definedItems.insert(item.name()); + + const auto &documentedItemList = enume->doc().enumItemNames(); + QSet<QString> documentedItems(documentedItemList.cbegin(), documentedItemList.cend()); + const QSet<QString> allItems = definedItems + documentedItems; + if (allItems.size() > definedItems.size() + || allItems.size() > documentedItems.size()) { + for (const auto &it : allItems) { + if (!definedItems.contains(it)) { + QString details; + QString best = nearestName(it, definedItems); + if (!best.isEmpty() && !documentedItems.contains(best)) + details = QStringLiteral("Maybe you meant '%1'?").arg(best); + + node->doc().location().warning( + QStringLiteral("No such enum item '%1' in %2") + .arg(it, node->plainFullName()), + details); + } else if (!documentedItems.contains(it)) { + node->doc().location().warning( + QStringLiteral("Undocumented enum item '%1' in %2") + .arg(it, node->plainFullName())); + } + } + } + } else if (fn) { + const QSet<QString> declaredNames = fn->parameters().getNames(); + const QSet<QString> documentedNames = fn->doc().parameterNames(); + if (declaredNames != documentedNames) { + for (const auto &name : declaredNames) { + if (!documentedNames.contains(name)) { + if (fn->isActive() || fn->isPreliminary()) { + // Require no parameter documentation for overrides and overloads, + // and only require it for non-overloaded constructors. + if (!fn->isMarkedReimp() && !fn->isOverload() && + !(fn->isSomeCtor() && fn->hasOverloads())) { + fn->doc().location().warning( + QStringLiteral("Undocumented parameter '%1' in %2") + .arg(name, node->plainFullName())); + } + } + } + } + for (const auto &name : documentedNames) { + if (!declaredNames.contains(name)) { + QString best = nearestName(name, declaredNames); + QString details; + if (!best.isEmpty()) + details = QStringLiteral("Maybe you meant '%1'?").arg(best); + fn->doc().location().warning(QStringLiteral("No such parameter '%1' in %2") + .arg(name, fn->plainFullName()), + details); + } + } + } + /* + This return value check should be implemented + for all functions with a return type. + mws 13/12/2018 + */ + if (!fn->isDeprecated() && fn->returnsBool() && !fn->isMarkedReimp() + && !fn->isOverload()) { + if (!fn->doc().body().contains("return")) + node->doc().location().warning( + QStringLiteral("Undocumented return value " + "(hint: use 'return' or 'returns' in the text")); + } + } + } + generateEnumValuesForQmlProperty(node, marker); + generateRequiredLinks(node, marker); +} + +/*! + Generates either a link to the project folder for example \a node, or a list + of links files/images if 'url.examples config' variable is not defined. + + Does nothing for non-example nodes. +*/ +void Generator::generateRequiredLinks(const Node *node, CodeMarker *marker) +{ + if (!node->isExample()) + return; + + const auto *en = static_cast<const ExampleNode *>(node); + QString exampleUrl{Config::instance().get(CONFIG_URL + Config::dot + CONFIG_EXAMPLES).asString()}; + + if (exampleUrl.isEmpty()) { + if (!en->noAutoList()) { + generateFileList(en, marker, false); // files + generateFileList(en, marker, true); // images + } + } else { + generateLinkToExample(en, marker, exampleUrl); + } +} + +/*! + Generates an external link to the project folder for example \a node. + The path to the example replaces a placeholder '\1' character if + one is found in the \a baseUrl string. If no such placeholder is found, + the path is appended to \a baseUrl, after a '/' character if \a baseUrl did + not already end in one. +*/ +void Generator::generateLinkToExample(const ExampleNode *en, CodeMarker *marker, + const QString &baseUrl) +{ + QString exampleUrl(baseUrl); + QString link; +#ifndef QT_BOOTSTRAPPED + link = QUrl(exampleUrl).host(); +#endif + if (!link.isEmpty()) + link.prepend(" @ "); + link.prepend("Example project"); + + const QLatin1Char separator('/'); + const QLatin1Char placeholder('\1'); + if (!exampleUrl.contains(placeholder)) { + if (!exampleUrl.endsWith(separator)) + exampleUrl += separator; + exampleUrl += placeholder; + } + + // Construct a path to the example; <install path>/<example name> + QString pathRoot; + QStringMultiMap *metaTagMap = en->doc().metaTagMap(); + if (metaTagMap) + pathRoot = metaTagMap->value(QLatin1String("installpath")); + if (pathRoot.isEmpty()) + pathRoot = Config::instance().get(CONFIG_EXAMPLESINSTALLPATH).asString(); + QStringList path = QStringList() << pathRoot << en->name(); + path.removeAll(QString()); + + Text text; + text << Atom::ParaLeft + << Atom(Atom::Link, exampleUrl.replace(placeholder, path.join(separator))) + << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << Atom(Atom::String, link) + << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << Atom::ParaRight; + + generateText(text, nullptr, marker); +} + +void Generator::addImageToCopy(const ExampleNode *en, const ResolvedFile& resolved_file) +{ + QDir dirInfo; + // TODO: [uncentralized-output-directory-structure] + const QString prefix("/images/used-in-examples"); + + // TODO: Generators probably should not need to keep track of which files were generated. + // Understand if we really need this information and where it should + // belong, considering that it should be part of whichever system + // would actually store the file itself. + s_outFileNames << prefix.mid(1) + "/" + resolved_file.get_query(); + + + // TODO: [uncentralized-output-directory-structure] + QString imgOutDir = s_outDir + prefix + "/" + QFileInfo{resolved_file.get_query()}.path(); + if (!dirInfo.mkpath(imgOutDir)) + en->location().fatal(QStringLiteral("Cannot create output directory '%1'").arg(imgOutDir)); + Config::copyFile(en->location(), resolved_file.get_path(), QFileInfo{resolved_file.get_query()}.fileName(), imgOutDir); +} + +// TODO: [multi-purpose-function-with-flag][generate-file-list] +// Avoid the use of a boolean flag to dispatch to the correct +// implementation trough branching. +// We always have to process both images and files, such that we +// should consider to remove the branching altogheter, performing both +// operations in a single call. +// Otherwise, if this turns out to be infeasible, complex or +// possibly-confusing, consider extracting the processing code outside +// the function and provide two higer-level dispathing functions for +// files and images. + +/*! + This function is called when the documentation for an example is + being formatted. It outputs a list of files for the example, which + can be the example's source files or the list of images used by the + example. The images are copied into a subtree of + \c{...doc/html/images/used-in-examples/...} +*/ +void Generator::generateFileList(const ExampleNode *en, CodeMarker *marker, bool images) +{ + Text text; + OpenedList openedList(OpenedList::Bullet); + QString tag; + QStringList paths; + Atom::AtomType atomType = Atom::ExampleFileLink; + + if (images) { + paths = en->images(); + tag = "Images:"; + atomType = Atom::ExampleImageLink; + } else { // files + paths = en->files(); + tag = "Files:"; + } + std::sort(paths.begin(), paths.end(), Generator::comparePaths); + + text << Atom::ParaLeft << tag << Atom::ParaRight; + text << Atom(Atom::ListLeft, openedList.styleString()); + + for (const auto &path : std::as_const(paths)) { + auto maybe_resolved_file{file_resolver.resolve(path)}; + if (!maybe_resolved_file) { + // TODO: [uncentralized-admonition][failed-resolve-file] + QString details = std::transform_reduce( + file_resolver.get_search_directories().cbegin(), + file_resolver.get_search_directories().cend(), + u"Searched directories:"_s, + std::plus(), + [](const DirectoryPath &directory_path) -> QString { return u' ' + directory_path.value(); } + ); + + en->location().warning(u"(Generator)Cannot find file to quote from: %1"_s.arg(path), details); + + continue; + } + + auto file{*maybe_resolved_file}; + if (images) addImageToCopy(en, file); + else generateExampleFilePage(en, file, marker); + + openedList.next(); + text << Atom(Atom::ListItemNumber, openedList.numberString()) + << Atom(Atom::ListItemLeft, openedList.styleString()) << Atom::ParaLeft + << Atom(atomType, file.get_query()) << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << file.get_query() + << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << Atom::ParaRight + << Atom(Atom::ListItemRight, openedList.styleString()); + } + text << Atom(Atom::ListRight, openedList.styleString()); + if (!paths.isEmpty()) + generateText(text, en, marker); +} + +/*! + Recursive writing of HTML files from the root \a node. + */ +void Generator::generateDocumentation(Node *node) +{ + if (!node->url().isNull()) + return; + if (node->isIndexNode()) + return; + if (node->isInternal() && !m_showInternal) + return; + if (node->isExternalPage()) + return; + + /* + Obtain a code marker for the source file. + */ + CodeMarker *marker = CodeMarker::markerForFileName(node->location().filePath()); + + if (node->parent() != nullptr) { + if (node->isCollectionNode()) { + /* + A collection node collects: groups, C++ modules, or QML + modules. Testing for a CollectionNode must be done + before testing for a TextPageNode because a + CollectionNode is a PageNode at this point. + + Don't output an HTML page for the collection node unless + the \group, \module, or \qmlmodule command was actually + seen by qdoc in the qdoc comment for the node. + + A key prerequisite in this case is the call to + mergeCollections(cn). We must determine whether this + group, module or QML module has members in other + modules. We know at this point that cn's members list + contains only members in the current module. Therefore, + before outputting the page for cn, we must search for + members of cn in the other modules and add them to the + members list. + */ + auto *cn = static_cast<CollectionNode *>(node); + if (cn->wasSeen()) { + m_qdb->mergeCollections(cn); + beginSubPage(node, fileName(node)); + generateCollectionNode(cn, marker); + endSubPage(); + } else if (cn->isGenericCollection()) { + // Currently used only for the module's related orphans page + // but can be generalized for other kinds of collections if + // other use cases pop up. + QString name = cn->name().toLower(); + name.replace(QChar(' '), QString("-")); + QString filename = + cn->tree()->physicalModuleName() + "-" + name + "." + fileExtension(); + beginSubPage(node, filename); + generateGenericCollectionPage(cn, marker); + endSubPage(); + } + } else if (node->isTextPageNode()) { + beginSubPage(node, fileName(node)); + generatePageNode(static_cast<PageNode *>(node), marker); + endSubPage(); + } else if (node->isAggregate()) { + if ((node->isClassNode() || node->isHeader() || node->isNamespace()) + && node->docMustBeGenerated()) { + beginSubPage(node, fileName(node)); + generateCppReferencePage(static_cast<Aggregate *>(node), marker); + endSubPage(); + } else if (node->isQmlType()) { + beginSubPage(node, fileName(node)); + auto *qcn = static_cast<QmlTypeNode *>(node); + generateQmlTypePage(qcn, marker); + endSubPage(); + } else if (node->isProxyNode()) { + beginSubPage(node, fileName(node)); + generateProxyPage(static_cast<Aggregate *>(node), marker); + endSubPage(); + } + } + } + + if (node->isAggregate()) { + auto *aggregate = static_cast<Aggregate *>(node); + const NodeList &children = aggregate->childNodes(); + for (auto *child : children) { + if (child->isPageNode() && !child->isPrivate()) { + generateDocumentation(child); + } else if (!node->parent() && child->isInAPI() && !child->isRelatedNonmember()) { + // Warn if there are documented non-page-generating nodes in the root namespace + child->location().warning(u"No documentation generated for %1 '%2' in global scope."_s + .arg(typeString(child), child->name()), + u"Maybe you forgot to use the '\\relates' command?"_s); + child->setStatus(Node::DontDocument); + } else if (child->isQmlModule() && !child->wasSeen()) { + // An undocumented QML module that was constructed as a placeholder + auto *qmlModule = static_cast<CollectionNode *>(child); + for (const auto *member : qmlModule->members()) { + member->location().warning( + u"Undocumented QML module '%1' referred by type '%2' or its members"_s + .arg(qmlModule->name(), member->name()), + u"Maybe you forgot to document '\\qmlmodule %1'?"_s + .arg(qmlModule->name())); + } + } else if (child->isQmlType() && !child->hasDoc()) { + // A placeholder QML type with incorrect module identifier + auto *qmlType = static_cast<QmlTypeNode *>(child); + if (auto qmid = qmlType->logicalModuleName(); !qmid.isEmpty()) + qmlType->location().warning(u"No such type '%1' in QML module '%2'"_s + .arg(qmlType->name(), qmid)); + } + } + } +} + +void Generator::generateReimplementsClause(const FunctionNode *fn, CodeMarker *marker) +{ + if (fn->overridesThis().isEmpty() || !fn->parent()->isClassNode()) + return; + + auto *cn = static_cast<ClassNode *>(fn->parent()); + const FunctionNode *overrides = cn->findOverriddenFunction(fn); + if (overrides && !overrides->isPrivate() && !overrides->parent()->isPrivate()) { + if (overrides->hasDoc()) { + Text text; + text << Atom::ParaLeft << "Reimplements: "; + QString fullName = + overrides->parent()->name() + + "::" + overrides->signature(Node::SignaturePlain); + appendFullName(text, overrides->parent(), fullName, overrides); + text << "." << Atom::ParaRight; + generateText(text, fn, marker); + } else { + fn->doc().location().warning( + QStringLiteral("Illegal \\reimp; no documented virtual function for %1") + .arg(overrides->plainSignature())); + } + return; + } + const PropertyNode *sameName = cn->findOverriddenProperty(fn); + if (sameName && sameName->hasDoc()) { + Text text; + text << Atom::ParaLeft << "Reimplements an access function for property: "; + QString fullName = sameName->parent()->name() + "::" + sameName->name(); + appendFullName(text, sameName->parent(), fullName, sameName); + text << "." << Atom::ParaRight; + generateText(text, fn, marker); + } +} + +QString Generator::formatSince(const Node *node) +{ + QStringList since = node->since().split(QLatin1Char(' ')); + + // If there is only one argument, assume it is the Qt version number. + if (since.size() == 1) + return "Qt " + since[0]; + + // Otherwise, use the original <project> <version> string. + return node->since(); +} + +/*! + \internal + Returns a string representing status information of a \a node. + + If a status description is returned, it is one of: + \list + \li Custom status set explicitly in node's documentation using + \c {\meta {status} {<description>}}, + \li 'Deprecated [since <version>]' (\\deprecated [<version>]), + \li 'Until <version>', + \li 'Preliminary' (\\preliminary), or + \li The description adopted from associated module's state: + \c {\modulestate {<description>}}. + \endlist + + Otherwise, returns \c std::nullopt. +*/ +std::optional<QString> formatStatus(const Node *node, QDocDatabase *qdb) +{ + QString status; + + if (const auto metaMap = node->doc().metaTagMap(); metaMap) { + status = metaMap->value("status"); + if (!status.isEmpty()) + return {status}; + } + const auto since = node->deprecatedSince(); + if (node->status() == Node::Deprecated) { + status = u"Deprecated"_s; + if (!since.isEmpty()) + status += " since %1"_L1.arg(since); + } else if (!since.isEmpty()) { + status = "Until %1"_L1.arg(since); + } else if (node->status() == Node::Preliminary) { + status = u"Preliminary"_s; + } else if (const auto collection = qdb->getModuleNode(node); collection) { + status = collection->state(); + } + + return status.isEmpty() ? std::nullopt : std::optional(status); +} + +void Generator::generateSince(const Node *node, CodeMarker *marker) +{ + if (!node->since().isEmpty()) { + Text text; + text << Atom::ParaLeft << "This " << typeString(node) << " was introduced in " + << formatSince(node) << "." << Atom::ParaRight; + generateText(text, node, marker); + } +} + +void Generator::generateNoexceptNote(const Node* node, CodeMarker* marker) { + std::vector<const Node*> nodes; + if (node->isSharedCommentNode()) { + auto shared_node = static_cast<const SharedCommentNode*>(node); + nodes.reserve(shared_node->collective().size()); + nodes.insert(nodes.begin(), shared_node->collective().begin(), shared_node->collective().end()); + } else nodes.push_back(node); + + std::size_t counter{1}; + for (const Node* node : nodes) { + if (node->isFunction(Node::CPP)) { + if (auto exception_info = static_cast<const FunctionNode*>(node)->getNoexcept(); exception_info && !(*exception_info).isEmpty()) { + Text text; + text << Atom::NoteLeft + << (nodes.size() > 1 ? QString::fromStdString(" ("s + std::to_string(counter) + ")"s) : QString::fromStdString("This ") + typeString(node)) + << " does not throw any exception when " << "\"" << *exception_info << "\"" << " is true." + << Atom::NoteRight; + generateText(text, node, marker); + } + } + + ++counter; + } +} + +void Generator::generateStatus(const Node *node, CodeMarker *marker) +{ + Text text; + + switch (node->status()) { + case Node::Active: + // Output the module 'state' description if set. + if (node->isModule() || node->isQmlModule()) { + const QString &state = static_cast<const CollectionNode*>(node)->state(); + if (!state.isEmpty()) { + text << Atom::ParaLeft << "This " << typeString(node) << " is in " + << Atom(Atom::FormattingLeft, ATOM_FORMATTING_ITALIC) << state + << Atom(Atom::FormattingRight, ATOM_FORMATTING_ITALIC) << " state." + << Atom::ParaRight; + break; + } + } + if (const auto version = node->deprecatedSince(); !version.isEmpty()) { + text << Atom::ParaLeft << "This " << typeString(node) + << " is scheduled for deprecation in version " + << version << "." << Atom::ParaRight; + } + break; + case Node::Preliminary: + text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) << "This " + << typeString(node) << " is under development and is subject to change." + << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD) << Atom::ParaRight; + break; + case Node::Deprecated: + text << Atom::ParaLeft; + if (node->isAggregate()) + text << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD); + text << "This " << typeString(node) << " is deprecated"; + if (const QString &version = node->deprecatedSince(); !version.isEmpty()) { + text << " since "; + if (node->isQmlNode() && !node->logicalModuleName().isEmpty()) + text << node->logicalModuleName() << " "; + text << version; + } + + text << ". We strongly advise against using it in new code."; + if (node->isAggregate()) + text << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD); + text << Atom::ParaRight; + break; + case Node::Internal: + default: + break; + } + generateText(text, node, marker); +} + +/*! + Generates an addendum note of type \a type for \a node, using \a marker + as the code marker. +*/ +void Generator::generateAddendum(const Node *node, Addendum type, CodeMarker *marker, + bool generateNote) +{ + Q_ASSERT(node && !node->name().isEmpty()); + Text text; + text << Atom(Atom::DivLeft, + "class=\"admonition %1\""_L1.arg(generateNote ? u"note"_s : u"auto"_s)); + text << Atom::ParaLeft; + + if (generateNote) { + text << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) + << "Note: " << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD); + } + + switch (type) { + case Invokable: + text << "This function can be invoked via the meta-object system and from QML. See " + << Atom(Atom::AutoLink, "Q_INVOKABLE") + << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) + << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << "."; + break; + case PrivateSignal: + text << "This is a private signal. It can be used in signal connections " + "but cannot be emitted by the user."; + break; + case QmlSignalHandler: + { + QString handler(node->name()); + qsizetype prefixLocation = handler.lastIndexOf('.', -2) + 1; + handler[prefixLocation] = handler[prefixLocation].toTitleCase(); + handler.insert(prefixLocation, QLatin1String("on")); + text << "The corresponding handler is " + << Atom(Atom::FormattingLeft, ATOM_FORMATTING_TELETYPE) << handler + << Atom(Atom::FormattingRight, ATOM_FORMATTING_TELETYPE) << "."; + break; + } + case AssociatedProperties: + { + if (!node->isFunction()) + return; + const auto *fn = static_cast<const FunctionNode *>(node); + auto nodes = fn->associatedProperties(); + if (nodes.isEmpty()) + return; + std::sort(nodes.begin(), nodes.end(), Node::nodeNameLessThan); + for (const auto *n : std::as_const(nodes)) { + QString msg; + const auto *pn = static_cast<const PropertyNode *>(n); + switch (pn->role(fn)) { + case PropertyNode::FunctionRole::Getter: + msg = QStringLiteral("Getter function"); + break; + case PropertyNode::FunctionRole::Setter: + msg = QStringLiteral("Setter function"); + break; + case PropertyNode::FunctionRole::Resetter: + msg = QStringLiteral("Resetter function"); + break; + case PropertyNode::FunctionRole::Notifier: + msg = QStringLiteral("Notifier signal"); + break; + default: + continue; + } + text << msg << " for property " << Atom(Atom::Link, pn->name()) + << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << pn->name() + << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << ". "; + } + break; + } + case BindableProperty: + { + text << "This property supports " + << Atom(Atom::Link, "QProperty") + << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << "QProperty" + << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); + text << " bindings."; + break; + } + default: + return; + } + + text << Atom::ParaRight + << Atom::DivRight; + generateText(text, node, marker); +} + +/*! + Generate the documentation for \a relative. i.e. \a relative + is the node that represents the entity where a qdoc comment + was found, and \a text represents the qdoc comment. + */ +bool Generator::generateText(const Text &text, const Node *relative, CodeMarker *marker) +{ + bool result = false; + if (text.firstAtom() != nullptr) { + int numAtoms = 0; + initializeTextOutput(); + generateAtomList(text.firstAtom(), relative, marker, true, numAtoms); + result = true; + } + return result; +} + +/* + The node is an aggregate, typically a class node, which has + a threadsafeness level. This function checks all the children + of the node to see if they are exceptions to the node's + threadsafeness. If there are any exceptions, the exceptions + are added to the appropriate set (reentrant, threadsafe, and + nonreentrant, and true is returned. If there are no exceptions, + the three node lists remain empty and false is returned. + */ +bool Generator::hasExceptions(const Node *node, NodeList &reentrant, NodeList &threadsafe, + NodeList &nonreentrant) +{ + bool result = false; + Node::ThreadSafeness ts = node->threadSafeness(); + const NodeList &children = static_cast<const Aggregate *>(node)->childNodes(); + for (auto child : children) { + if (!child->isDeprecated()) { + switch (child->threadSafeness()) { + case Node::Reentrant: + reentrant.append(child); + if (ts == Node::ThreadSafe) + result = true; + break; + case Node::ThreadSafe: + threadsafe.append(child); + if (ts == Node::Reentrant) + result = true; + break; + case Node::NonReentrant: + nonreentrant.append(child); + result = true; + break; + default: + break; + } + } + } + return result; +} + +/*! + Returns \c true if a trademark symbol should be appended to the + output as determined by \a atom. Trademarks are tracked via the + use of the \\tm formatting command. + + Returns true if: + + \list + \li \a atom is of type Atom::FormattingRight containing + ATOM_FORMATTING_TRADEMARK, and + \li The trademarked string is the first appearance on the + current sub-page. + \endlist +*/ +bool Generator::appendTrademark(const Atom *atom) +{ + if (atom->type() != Atom::FormattingRight) + return false; + if (atom->string() != ATOM_FORMATTING_TRADEMARK) + return false; + + if (atom->count() > 1) { + if (s_trademarks.contains(atom->string(1))) + return false; + s_trademarks << atom->string(1); + } + + return true; +} + +static void startNote(Text &text) +{ + text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) + << "Note:" << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD) << " "; +} + +/*! + Generates text that explains how threadsafe and/or reentrant + \a node is. + */ +void Generator::generateThreadSafeness(const Node *node, CodeMarker *marker) +{ + Text text, rlink, tlink; + NodeList reentrant; + NodeList threadsafe; + NodeList nonreentrant; + Node::ThreadSafeness ts = node->threadSafeness(); + bool exceptions = false; + + rlink << Atom(Atom::Link, "reentrant") << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) + << "reentrant" << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); + + tlink << Atom(Atom::Link, "thread-safe") << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) + << "thread-safe" << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); + + switch (ts) { + case Node::UnspecifiedSafeness: + break; + case Node::NonReentrant: + text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) + << "Warning:" << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD) << " This " + << typeString(node) << " is not " << rlink << "." << Atom::ParaRight; + break; + case Node::Reentrant: + case Node::ThreadSafe: + startNote(text); + if (node->isAggregate()) { + exceptions = hasExceptions(node, reentrant, threadsafe, nonreentrant); + text << "All functions in this " << typeString(node) << " are "; + if (ts == Node::ThreadSafe) + text << tlink; + else + text << rlink; + + if (!exceptions || (ts == Node::Reentrant && !threadsafe.isEmpty())) + text << "."; + else + text << " with the following exceptions:"; + } else { + text << "This " << typeString(node) << " is "; + if (ts == Node::ThreadSafe) + text << tlink; + else + text << rlink; + text << "."; + } + text << Atom::ParaRight; + break; + default: + break; + } + generateText(text, node, marker); + + if (exceptions) { + text.clear(); + if (ts == Node::Reentrant) { + if (!nonreentrant.isEmpty()) { + startNote(text); + text << "These functions are not " << rlink << ":" << Atom::ParaRight; + signatureList(nonreentrant, node, marker); + } + if (!threadsafe.isEmpty()) { + text.clear(); + startNote(text); + text << "These functions are also " << tlink << ":" << Atom::ParaRight; + generateText(text, node, marker); + signatureList(threadsafe, node, marker); + } + } else { // thread-safe + if (!reentrant.isEmpty()) { + startNote(text); + text << "These functions are only " << rlink << ":" << Atom::ParaRight; + signatureList(reentrant, node, marker); + } + if (!nonreentrant.isEmpty()) { + text.clear(); + startNote(text); + text << "These functions are not " << rlink << ":" << Atom::ParaRight; + signatureList(nonreentrant, node, marker); + } + } + } +} + +/*! + \internal + + Generates text that describes the comparison category of \a node. + The CodeMarker \a marker is passed along to generateText(). + */ +bool Generator::generateComparisonCategory(const Node *node, CodeMarker *marker) +{ + auto category{node->comparisonCategory()}; + if (category == ComparisonCategory::None) + return false; + + Text text; + text << Atom::ParaLeft << "This %1 is "_L1.arg(typeString(node)) + << Atom(Atom::FormattingLeft, ATOM_FORMATTING_ITALIC) + << QString::fromStdString(comparisonCategoryAsString(category)) + << ((category == ComparisonCategory::Equality) ? "-"_L1 : "ly "_L1) + << Atom(Atom::String, "comparable"_L1) + << Atom(Atom::FormattingRight, ATOM_FORMATTING_ITALIC) + << "."_L1 << Atom::ParaRight; + generateText(text, node, marker); + return true; +} + +/*! + Generates a list of types that compare to \a node with the comparison + category that applies for the relationship, followed by (an optional) + descriptive text. + + Returns \c true if text was generated, \c false otherwise. + */ +bool Generator::generateComparisonList(const Node *node) +{ + Q_ASSERT(node); + if (!node->doc().comparesWithMap()) + return false; + + Text relationshipText; + for (auto [key, description] : node->doc().comparesWithMap()->asKeyValueRange()) { + const QString &category = QString::fromStdString(comparisonCategoryAsString(key)); + + relationshipText << Atom::ParaLeft << "This %1 is "_L1.arg(typeString(node)) + << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) << category + << ((key == ComparisonCategory::Equality) ? "-"_L1 : "ly "_L1) + << "comparable"_L1 + << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD) + << " with "_L1; + + const QStringList types{description.firstAtom()->string().split(';'_L1)}; + for (const auto &name : types) + relationshipText << Atom(Atom::AutoLink, name) + << Utilities::separator(types.indexOf(name), types.size()); + + relationshipText << Atom(Atom::ParaRight) << description; + } + + generateText(relationshipText, node, nullptr); + return !relationshipText.isEmpty(); +} + +/*! + Returns the string containing an example code of the input node, + if it is an overloaded signal. Otherwise, returns an empty string. + */ +QString Generator::getOverloadedSignalCode(const Node *node) +{ + if (!node->isFunction()) + return QString(); + const auto func = static_cast<const FunctionNode *>(node); + if (!func->isSignal() || !func->hasOverloads()) + return QString(); + + // Compute a friendly name for the object of that instance. + // e.g: "QAbstractSocket" -> "abstractSocket" + QString objectName = node->parent()->name(); + if (objectName.size() >= 2) { + if (objectName[0] == 'Q') + objectName = objectName.mid(1); + objectName[0] = objectName[0].toLower(); + } + + // We have an overloaded signal, show an example. Note, for const + // overloaded signals, one should use Q{Const,NonConst}Overload, but + // it is very unlikely that we will ever have public API overloading + // signals by const. + QString code = "connect(" + objectName + ", QOverload<"; + code += func->parameters().generateTypeList(); + code += ">::of(&" + func->parent()->name() + "::" + func->name() + "),\n [=]("; + code += func->parameters().generateTypeAndNameList(); + code += "){ /* ... */ });"; + + return code; +} + +/*! + If the node is an overloaded signal, add a node with an example on how to connect to it + */ +void Generator::generateOverloadedSignal(const Node *node, CodeMarker *marker) +{ + QString code = getOverloadedSignalCode(node); + if (code.isEmpty()) + return; + + Text text; + text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) + << "Note:" << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD) << " Signal " + << Atom(Atom::FormattingLeft, ATOM_FORMATTING_ITALIC) << node->name() + << Atom(Atom::FormattingRight, ATOM_FORMATTING_ITALIC) + << " is overloaded in this class. " + "To connect to this signal by using the function pointer syntax, Qt " + "provides a convenient helper for obtaining the function pointer as " + "shown in this example:" + << Atom(Atom::Code, marker->markedUpCode(code, node, node->location())); + + generateText(text, node, marker); +} + +/*! + Traverses the database recursively to generate all the documentation. + */ +void Generator::generateDocs() +{ + s_currentGenerator = this; + generateDocumentation(m_qdb->primaryTreeRoot()); +} + +Generator *Generator::generatorForFormat(const QString &format) +{ + for (const auto &generator : std::as_const(s_generators)) { + if (generator->format() == format) + return generator; + } + return nullptr; +} + +QString Generator::indent(int level, const QString &markedCode) +{ + if (level == 0) + return markedCode; + + QString t; + int column = 0; + + int i = 0; + while (i < markedCode.size()) { + if (markedCode.at(i) == QLatin1Char('\n')) { + column = 0; + } else { + if (column == 0) { + for (int j = 0; j < level; j++) + t += QLatin1Char(' '); + } + column++; + } + t += markedCode.at(i++); + } + return t; +} + +void Generator::initialize() +{ + Config &config = Config::instance(); + s_outputFormats = config.getOutputFormats(); + s_redirectDocumentationToDevNull = config.get(CONFIG_REDIRECTDOCUMENTATIONTODEVNULL).asBool(); + + for (auto &g : s_generators) { + if (s_outputFormats.contains(g->format())) { + s_currentGenerator = g; + g->initializeGenerator(); + } + } + + const auto &configFormatting = config.subVars(CONFIG_FORMATTING); + for (const auto &n : configFormatting) { + QString formattingDotName = CONFIG_FORMATTING + Config::dot + n; + const auto &formattingDotNames = config.subVars(formattingDotName); + for (const auto &f : formattingDotNames) { + const auto &configVar = config.get(formattingDotName + Config::dot + f); + QString def{configVar.asString()}; + if (!def.isEmpty()) { + int numParams = Config::numParams(def); + int numOccs = def.count("\1"); + if (numParams != 1) { + configVar.location().warning(QStringLiteral("Formatting '%1' must " + "have exactly one " + "parameter (found %2)") + .arg(n, numParams)); + } else if (numOccs > 1) { + configVar.location().fatal(QStringLiteral("Formatting '%1' must " + "contain exactly one " + "occurrence of '\\1' " + "(found %2)") + .arg(n, numOccs)); + } else { + int paramPos = def.indexOf("\1"); + s_fmtLeftMaps[f].insert(n, def.left(paramPos)); + s_fmtRightMaps[f].insert(n, def.mid(paramPos + 1)); + } + } + } + } + + s_project = config.get(CONFIG_PROJECT).asString(); + s_outDir = config.getOutputDir(); + s_outSubdir = s_outDir.mid(s_outDir.lastIndexOf('/') + 1); + + s_outputPrefixes.clear(); + QStringList items{config.get(CONFIG_OUTPUTPREFIXES).asStringList()}; + if (!items.isEmpty()) { + for (const auto &prefix : items) + s_outputPrefixes[prefix] = + config.get(CONFIG_OUTPUTPREFIXES + Config::dot + prefix).asString(); + } + if (!items.contains(u"QML"_s)) + s_outputPrefixes[u"QML"_s] = u"qml-"_s; + + s_outputSuffixes.clear(); + for (const auto &suffix : config.get(CONFIG_OUTPUTSUFFIXES).asStringList()) + s_outputSuffixes[suffix] = config.get(CONFIG_OUTPUTSUFFIXES + + Config::dot + suffix).asString(); + + s_noLinkErrors = config.get(CONFIG_NOLINKERRORS).asBool(); + s_autolinkErrors = config.get(CONFIG_AUTOLINKERRORS).asBool(); +} + +/*! + Creates template-specific subdirs (e.g. /styles and /scripts for HTML) + and copies the files to them. + */ +void Generator::copyTemplateFiles(const QString &configVar, const QString &subDir) +{ + // TODO: [resolving-files-unlinked-to-doc] + // This is another case of resolving files, albeit it doesn't use Doc::resolveFile. + // While it may be left out of a first iteration of the file + // resolution logic, it should later be integrated into it. + // This should come naturally when the output directory logic is + // extracted and copying a file should require a validated + // intermediate format. + // Do note that what is done here is a bit different from the + // resolve file routine that is done for other user-given paths. + // Thas is, the paths will always be absolute and not relative as + // they are resolved from the configuration. + // Ideally, this could be solved in the configuration already, + // together with the other configuration resolution processes that + // do not abide by the same constraints that, for example, snippet + // resolution uses. + Config &config = Config::instance(); + QStringList files = config.getCanonicalPathList(configVar, Config::Validate); + const auto &loc = config.get(configVar).location(); + if (!files.isEmpty()) { + QDir dirInfo; + // TODO: [uncentralized-output-directory-structure] + // As with other places in the generation pass, the details of + // where something is saved in the output directory are spread + // to whichever part of the generation does the saving. + // It is hence complex to build a model of how an output + // directory looks like, as the knowledge has no specific + // entry point or chain-path that can be followed in full. + // Each of those operations should be centralized in a system + // that uniquely knows what the format of the output-directory + // is and how to perform operations on it. + // Later, move this operation to that centralized system. + QString templateDir = s_outDir + QLatin1Char('/') + subDir; + if (!dirInfo.exists(templateDir) && !dirInfo.mkdir(templateDir)) { + // TODO: [uncentralized-admonition] + loc.fatal(QStringLiteral("Cannot create %1 directory '%2'").arg(subDir, templateDir)); + } else { + for (const auto &file : files) { + if (!file.isEmpty()) + Config::copyFile(loc, file, file, templateDir); + } + } + } +} + +/*! + Reads format-specific variables from config, sets output + (sub)directories, creates them on the filesystem and copies the + template-specific files. + */ +void Generator::initializeFormat() +{ + Config &config = Config::instance(); + s_outFileNames.clear(); + s_useOutputSubdirs = true; + if (config.get(format() + Config::dot + "nosubdirs").asBool()) + resetUseOutputSubdirs(); + + if (s_outputFormats.isEmpty()) + return; + + s_outDir = config.getOutputDir(format()); + if (s_outDir.isEmpty()) { + Location().fatal(QStringLiteral("No output directory specified in " + "configuration file or on the command line")); + } else { + s_outSubdir = s_outDir.mid(s_outDir.lastIndexOf('/') + 1); + } + + QDir outputDir(s_outDir); + if (outputDir.exists()) { + if (!config.generating() && Generator::useOutputSubdirs()) { + if (!outputDir.isEmpty()) + Location().error(QStringLiteral("Output directory '%1' exists but is not empty") + .arg(s_outDir)); + } + } else if (!outputDir.mkpath(QStringLiteral("."))) { + Location().fatal(QStringLiteral("Cannot create output directory '%1'").arg(s_outDir)); + } + + // Output directory exists, which is enough for prepare phase. + if (config.preparing()) + return; + + const QLatin1String imagesDir("images"); + if (!outputDir.exists(imagesDir) && !outputDir.mkdir(imagesDir)) + Location().fatal(QStringLiteral("Cannot create images directory '%1'").arg(outputDir.filePath(imagesDir))); + + copyTemplateFiles(format() + Config::dot + CONFIG_STYLESHEETS, "style"); + copyTemplateFiles(format() + Config::dot + CONFIG_SCRIPTS, "scripts"); + copyTemplateFiles(format() + Config::dot + CONFIG_EXTRAIMAGES, "images"); + + // Use a format-specific .quotinginformation if defined, otherwise a global value + if (config.subVars(format()).contains(CONFIG_QUOTINGINFORMATION)) + m_quoting = config.get(format() + Config::dot + CONFIG_QUOTINGINFORMATION).asBool(); + else + m_quoting = config.get(CONFIG_QUOTINGINFORMATION).asBool(); +} + +/*! + Updates the generator's m_showInternal from the Config. + */ +void Generator::initializeGenerator() +{ + m_showInternal = Config::instance().showInternal(); +} + +bool Generator::matchAhead(const Atom *atom, Atom::AtomType expectedAtomType) +{ + return atom->next() && atom->next()->type() == expectedAtomType; +} + +/*! + Used for writing to the current output stream. Returns a + reference to the current output stream, which is then used + with the \c {<<} operator for writing. + */ +QTextStream &Generator::out() +{ + return *outStreamStack.top(); +} + +QString Generator::outFileName() +{ + return QFileInfo(static_cast<QFile *>(out().device())->fileName()).fileName(); +} + +QString Generator::outputPrefix(const Node *node) +{ + // Omit prefix for module pages + if (node->isPageNode() && !node->isCollectionNode()) { + switch (node->genus()) { + case Node::QML: + return s_outputPrefixes[u"QML"_s]; + case Node::CPP: + return s_outputPrefixes[u"CPP"_s]; + default: + break; + } + } + return QString(); +} + +QString Generator::outputSuffix(const Node *node) +{ + if (node->isPageNode()) { + switch (node->genus()) { + case Node::QML: + return s_outputSuffixes[u"QML"_s]; + case Node::CPP: + return s_outputSuffixes[u"CPP"_s]; + default: + break; + } + } + + return QString(); +} + +bool Generator::parseArg(const QString &src, const QString &tag, int *pos, int n, + QStringView *contents, QStringView *par1) +{ +#define SKIP_CHAR(c) \ + if (i >= n || src[i] != c) \ + return false; \ + ++i; + +#define SKIP_SPACE \ + while (i < n && src[i] == ' ') \ + ++i; + + qsizetype i = *pos; + qsizetype j {}; + + // assume "<@" has been parsed outside + // SKIP_CHAR('<'); + // SKIP_CHAR('@'); + + if (tag != QStringView(src).mid(i, tag.size())) { + return false; + } + + // skip tag + i += tag.size(); + + // parse stuff like: linkTag("(<@link node=\"([^\"]+)\">).*(</@link>)"); + if (par1) { + SKIP_SPACE; + // read parameter name + j = i; + while (i < n && src[i].isLetter()) + ++i; + if (src[i] == '=') { + SKIP_CHAR('='); + SKIP_CHAR('"'); + // skip parameter name + j = i; + while (i < n && src[i] != '"') + ++i; + *par1 = QStringView(src).mid(j, i - j); + SKIP_CHAR('"'); + SKIP_SPACE; + } + } + SKIP_SPACE; + SKIP_CHAR('>'); + + // find contents up to closing "</@tag> + j = i; + for (; true; ++i) { + if (i + 4 + tag.size() > n) + return false; + if (src[i] != '<') + continue; + if (src[i + 1] != '/') + continue; + if (src[i + 2] != '@') + continue; + if (tag != QStringView(src).mid(i + 3, tag.size())) + continue; + if (src[i + 3 + tag.size()] != '>') + continue; + break; + } + + *contents = QStringView(src).mid(j, i - j); + + i += tag.size() + 4; + + *pos = i; + return true; +#undef SKIP_CHAR +#undef SKIP_SPACE +} + +QString Generator::plainCode(const QString &markedCode) +{ + QString t = markedCode; + t.replace(tag, QString()); + t.replace(quot, QLatin1String("\"")); + t.replace(gt, QLatin1String(">")); + t.replace(lt, QLatin1String("<")); + t.replace(amp, QLatin1String("&")); + return t; +} + +int Generator::skipAtoms(const Atom *atom, Atom::AtomType type) const +{ + int skipAhead = 0; + atom = atom->next(); + while (atom && atom->type() != type) { + skipAhead++; + atom = atom->next(); + } + return skipAhead; +} + +/*! + Resets the variables used during text output. + */ +void Generator::initializeTextOutput() +{ + m_inLink = false; + m_inContents = false; + m_inSectionHeading = false; + m_inTableHeader = false; + m_numTableRows = 0; + m_threeColumnEnumValueTable = true; + m_link.clear(); + m_sectionNumber.clear(); +} + +void Generator::supplementAlsoList(const Node *node, QList<Text> &alsoList) +{ + if (node->isFunction() && !node->isMacro()) { + const auto fn = static_cast<const FunctionNode *>(node); + if (fn->overloadNumber() == 0) { + QString alternateName; + const FunctionNode *alternateFunc = nullptr; + + if (fn->name().startsWith("set") && fn->name().size() >= 4) { + alternateName = fn->name()[3].toLower(); + alternateName += fn->name().mid(4); + alternateFunc = fn->parent()->findFunctionChild(alternateName, QString()); + + if (!alternateFunc) { + alternateName = "is" + fn->name().mid(3); + alternateFunc = fn->parent()->findFunctionChild(alternateName, QString()); + if (!alternateFunc) { + alternateName = "has" + fn->name().mid(3); + alternateFunc = fn->parent()->findFunctionChild(alternateName, QString()); + } + } + } else if (!fn->name().isEmpty()) { + alternateName = "set"; + alternateName += fn->name()[0].toUpper(); + alternateName += fn->name().mid(1); + alternateFunc = fn->parent()->findFunctionChild(alternateName, QString()); + } + + if (alternateFunc && alternateFunc->access() != Access::Private) { + int i; + for (i = 0; i < alsoList.size(); ++i) { + if (alsoList.at(i).toString().contains(alternateName)) + break; + } + + if (i == alsoList.size()) { + if (alternateFunc->isDeprecated() && !fn->isDeprecated()) + return; + alternateName += "()"; + + Text also; + also << Atom(Atom::Link, alternateName) + << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << alternateName + << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); + alsoList.prepend(also); + } + } + } + } +} + +void Generator::generateEnumValuesForQmlProperty(const Node *node, CodeMarker *marker) +{ + if (!node->isQmlProperty()) + return; + + const auto *qpn = static_cast<const QmlPropertyNode*>(node); + + if (!qpn->enumNode()) + return; + + // Retrieve atoms from C++ enum \value list + const auto body{qpn->enumNode()->doc().body()}; + const auto *start{body.firstAtom()}; + Text text; + + while ((start = start->find(Atom::ListLeft, ATOM_LIST_VALUE))) { + const auto end = start->find(Atom::ListRight, ATOM_LIST_VALUE); + // Skip subsequent ListLeft atoms, collating multiple lists into one + text << body.subText(text.isEmpty() ? start : start->next(), end); + start = end; + } + if (text.isEmpty()) + return; + + text << Atom(Atom::ListRight, ATOM_LIST_VALUE); + if (marker) + generateText(text, qpn, marker); + else + generateText(text, qpn); +} + +void Generator::terminate() +{ + for (const auto &generator : std::as_const(s_generators)) { + if (s_outputFormats.contains(generator->format())) + generator->terminateGenerator(); + } + + // REMARK: Generators currently, due to recent changes and the + // transitive nature of the current codebase, receive some of + // their dependencies in the constructor and some of them in their + // initialize-terminate lifetime. + // This means that generators need to be constructed and + // destructed between usages such that if multiple usages are + // required, the generators present in the list will have been + // destroyed by then such that accessing them would be an error. + // The current codebase calls initialize and the correspective + // terminate with the same scope as the lifetime of the + // generators. + // Then, clearing the list ensures that, if another generator + // execution is needed, the stale generators will not be removed + // as to be replaced by newly constructed ones. + // Do note that it is not clear that this will happen for any call + // in Qt's documentation and this should work only because of the + // form of the current codebase and the scoping of the + // initialize-terminate calls. As such, this should be considered + // a patchwork that may or may not be doing anything and that may + // break due to changes in other parts of the codebase. + // + // This is still to be considered temporary as the whole + // initialize-terminate idiom must be removed from the codebase. + s_generators.clear(); + + s_fmtLeftMaps.clear(); + s_fmtRightMaps.clear(); + s_outDir.clear(); +} + +void Generator::terminateGenerator() {} + +/*! + Trims trailing whitespace off the \a string and returns + the trimmed string. + */ +QString Generator::trimmedTrailing(const QString &string, const QString &prefix, + const QString &suffix) +{ + QString trimmed = string; + while (trimmed.size() > 0 && trimmed[trimmed.size() - 1].isSpace()) + trimmed.truncate(trimmed.size() - 1); + + trimmed.append(suffix); + trimmed.prepend(prefix); + return trimmed; +} + +QString Generator::typeString(const Node *node) +{ + switch (node->nodeType()) { + case Node::Namespace: + return "namespace"; + case Node::Class: + return "class"; + case Node::Struct: + return "struct"; + case Node::Union: + return "union"; + case Node::QmlType: + case Node::QmlValueType: + return "type"; + case Node::Page: + return "documentation"; + case Node::Enum: + return "enum"; + case Node::Typedef: + case Node::TypeAlias: + return "typedef"; + case Node::Function: { + const auto fn = static_cast<const FunctionNode *>(node); + switch (fn->metaness()) { + case FunctionNode::QmlSignal: + return "signal"; + case FunctionNode::QmlSignalHandler: + return "signal handler"; + case FunctionNode::QmlMethod: + return "method"; + case FunctionNode::MacroWithParams: + case FunctionNode::MacroWithoutParams: + return "macro"; + default: + break; + } + return "function"; + } + case Node::Property: + case Node::QmlProperty: + return "property"; + case Node::Module: + case Node::QmlModule: + return "module"; + case Node::SharedComment: { + const auto &collective = static_cast<const SharedCommentNode *>(node)->collective(); + return collective.first()->nodeTypeString(); + } + default: + return "documentation"; + } +} + +void Generator::unknownAtom(const Atom *atom) +{ + Location::internalError(QStringLiteral("unknown atom type '%1' in %2 generator") + .arg(atom->typeString(), format())); +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/generator.h b/src/qdoc/qdoc/src/qdoc/generator.h new file mode 100644 index 000000000..164882a8f --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/generator.h @@ -0,0 +1,212 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef GENERATOR_H +#define GENERATOR_H + +#include "text.h" +#include "utilities.h" +#include "filesystem/fileresolver.h" + +#include <QtCore/qlist.h> +#include <QtCore/qmap.h> +#include <QtCore/qstring.h> +#include <QtCore/qstringlist.h> +#include <QtCore/qtextstream.h> +#include <optional> + +QT_BEGIN_NAMESPACE + +typedef QMultiMap<QString, Node *> NodeMultiMap; + +class Aggregate; +class CodeMarker; +class ExampleNode; +class FunctionNode; +class Location; +class Node; +class QDocDatabase; + +class Generator +{ +public: + enum ListType { Generic, Obsolete }; + + enum Addendum { + Invokable, + PrivateSignal, + QmlSignalHandler, + AssociatedProperties, + BindableProperty + }; + + Generator(FileResolver& file_resolver); + virtual ~Generator(); + + virtual bool canHandleFormat(const QString &format) { return format == this->format(); } + virtual QString format() = 0; + virtual void generateDocs(); + virtual void initializeGenerator(); + virtual void initializeFormat(); + virtual void terminateGenerator(); + virtual QString typeString(const Node *node); + + QString fullDocumentLocation(const Node *node); + QString linkForExampleFile(const QString &path, const QString &fileExt = QString()); + static QString exampleFileTitle(const ExampleNode *relative, const QString &fileName); + static Generator *currentGenerator() { return s_currentGenerator; } + static Generator *generatorForFormat(const QString &format); + static void initialize(); + static const QString &outputDir() { return s_outDir; } + static const QString &outputSubdir() { return s_outSubdir; } + static void terminate(); + static const QStringList &outputFileNames() { return s_outFileNames; } + static bool noLinkErrors() { return s_noLinkErrors; } + static bool autolinkErrors() { return s_autolinkErrors; } + static QString defaultModuleName() { return s_project; } + static void resetUseOutputSubdirs() { s_useOutputSubdirs = false; } + static bool useOutputSubdirs() { return s_useOutputSubdirs; } + static void setQmlTypeContext(QmlTypeNode *t) { s_qmlTypeContext = t; } + static QmlTypeNode *qmlTypeContext() { return s_qmlTypeContext; } + static QString cleanRef(const QString &ref, bool xmlCompliant = false); + static QString plainCode(const QString &markedCode); + virtual QString fileBase(const Node *node) const; + +protected: + static QFile *openSubPageFile(const Node *node, const QString &fileName); + void beginSubPage(const Node *node, const QString &fileName); + void endSubPage(); + [[nodiscard]] virtual QString fileExtension() const = 0; + virtual void generateExampleFilePage(const Node *, ResolvedFile, CodeMarker * = nullptr) {} + virtual void generateAlsoList(const Node *node, CodeMarker *marker); + virtual void generateAlsoList(const Node *node) { generateAlsoList(node, nullptr); } + virtual qsizetype generateAtom(const Atom *, const Node *, CodeMarker *) { return 0; } + virtual void generateBody(const Node *node, CodeMarker *marker); + virtual void generateCppReferencePage(Aggregate *, CodeMarker *) {} + virtual void generateProxyPage(Aggregate *, CodeMarker *) {} + virtual void generateQmlTypePage(QmlTypeNode *, CodeMarker *) {} + virtual void generatePageNode(PageNode *, CodeMarker *) {} + virtual void generateCollectionNode(CollectionNode *, CodeMarker *) {} + virtual void generateGenericCollectionPage(CollectionNode *, CodeMarker *) {} + virtual void generateDocumentation(Node *node); + virtual bool generateText(const Text &text, const Node *relative, CodeMarker *marker); + virtual bool generateText(const Text &text, const Node *relative) + { + return generateText(text, relative, nullptr); + }; + virtual int skipAtoms(const Atom *atom, Atom::AtomType type) const; + + static bool matchAhead(const Atom *atom, Atom::AtomType expectedAtomType); + static QString outputPrefix(const Node *node); + static QString outputSuffix(const Node *node); + static void supplementAlsoList(const Node *node, QList<Text> &alsoList); + static QString trimmedTrailing(const QString &string, const QString &prefix, + const QString &suffix); + void initializeTextOutput(); + QString fileName(const Node *node, const QString &extension = QString()) const; + QMap<QString, QString> &formattingLeftMap(); + QMap<QString, QString> &formattingRightMap(); + const Atom *generateAtomList(const Atom *atom, const Node *relative, CodeMarker *marker, + bool generate, int &numGeneratedAtoms); + void generateEnumValuesForQmlProperty(const Node *node, CodeMarker *marker); + void generateRequiredLinks(const Node *node, CodeMarker *marker); + void generateLinkToExample(const ExampleNode *en, CodeMarker *marker, + const QString &exampleUrl); + virtual void generateFileList(const ExampleNode *en, CodeMarker *marker, bool images); + static QString formatSince(const Node *node); + void generateSince(const Node *node, CodeMarker *marker); + void generateNoexceptNote(const Node *node, CodeMarker *marker); + void generateStatus(const Node *node, CodeMarker *marker); + virtual void generateAddendum(const Node *node, Addendum type, CodeMarker *marker, + bool generateNote); + virtual void generateAddendum(const Node *node, Addendum type, CodeMarker *marker) + { + generateAddendum(node, type, marker, true); + }; + void generateThreadSafeness(const Node *node, CodeMarker *marker); + bool generateComparisonCategory(const Node *node, CodeMarker *marker = nullptr); + bool generateComparisonList(const Node *node); + + void generateOverloadedSignal(const Node *node, CodeMarker *marker); + static QString getOverloadedSignalCode(const Node *node); + QString indent(int level, const QString &markedCode); + QTextStream &out(); + QString outFileName(); + bool parseArg(const QString &src, const QString &tag, int *pos, int n, QStringView *contents, + QStringView *par1 = nullptr); + void unknownAtom(const Atom *atom); + int appendSortedQmlNames(Text &text, const Node *base, const NodeList &subs); + + static bool hasExceptions(const Node *node, NodeList &reentrant, NodeList &threadsafe, + NodeList &nonreentrant); + + QString naturalLanguage; + QString tagFile_; + QStack<QTextStream *> outStreamStack; + + void appendFullName(Text &text, const Node *apparentNode, const Node *relative, + const Node *actualNode = nullptr); + void appendFullName(Text &text, const Node *apparentNode, const QString &fullName, + const Node *actualNode); + int appendSortedNames(Text &text, const ClassNode *classe, + const QList<RelatedClass> &classes); + void appendSignature(Text &text, const Node *node); + void signatureList(const NodeList &nodes, const Node *relative, CodeMarker *marker); + + void addImageToCopy(const ExampleNode *en, const ResolvedFile& resolved_file); + // TODO: This seems to be used as the predicate in std::sort calls. + // Remove it as it is unneeded. + // Indeed, it could be replaced by std::less and, furthermore, + // std::sort already defaults to operator< when no predicate is + // provided. + static bool comparePaths(const QString &a, const QString &b) { return (a < b); } + static bool appendTrademark(const Atom *atom); + + static Qt::SortOrder sortOrder(const QString &str) + { + return (str == "descending") ? Qt::DescendingOrder : Qt::AscendingOrder; + } + +private: + static Generator *s_currentGenerator; + static QMap<QString, QMap<QString, QString>> s_fmtLeftMaps; + static QMap<QString, QMap<QString, QString>> s_fmtRightMaps; + static QList<Generator *> s_generators; + static QString s_project; + static QString s_outDir; + static QString s_outSubdir; + static QStringList s_outFileNames; + static QSet<QString> s_outputFormats; + static QSet<QString> s_trademarks; + static QHash<QString, QString> s_outputPrefixes; + static QHash<QString, QString> s_outputSuffixes; + static bool s_noLinkErrors; + static bool s_autolinkErrors; + static bool s_redirectDocumentationToDevNull; + static bool s_useOutputSubdirs; + static QmlTypeNode *s_qmlTypeContext; + + void generateReimplementsClause(const FunctionNode *fn, CodeMarker *marker); + static void copyTemplateFiles(const QString &configVar, const QString &subDir); + +protected: + FileResolver& file_resolver; + + QDocDatabase *m_qdb { nullptr }; + bool m_inLink { false }; + bool m_inContents { false }; + bool m_inSectionHeading { false }; + bool m_inTableHeader { false }; + bool m_threeColumnEnumValueTable { true }; + bool m_showInternal { false }; + bool m_quoting { false }; + int m_numTableRows { 0 }; + QString m_link {}; + QString m_sectionNumber {}; +}; + +std::optional<QString> formatStatus(const Node *node, QDocDatabase *qdb); + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/headernode.cpp b/src/qdoc/qdoc/src/qdoc/headernode.cpp new file mode 100644 index 000000000..ab576fbd6 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/headernode.cpp @@ -0,0 +1,43 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "headernode.h" + +QT_BEGIN_NAMESPACE + +/*! + \class Headernode + \brief This class represents a C++ header file. + */ + +HeaderNode::HeaderNode(Aggregate *parent, const QString &name) : Aggregate(HeaderFile, parent, name) +{ + // Set the include file with enclosing angle brackets removed + if (name.startsWith(QChar('<')) && name.size() > 2) + Aggregate::setIncludeFile(name.mid(1).chopped(1)); + else + Aggregate::setIncludeFile(name); +} + +/*! + Returns true if this header file node is not private and + contains at least one public child node with documentation. + */ +bool HeaderNode::docMustBeGenerated() const +{ + if (isInAPI()) + return true; + return hasDocumentedChildren(); +} + +/*! + Returns true if this header file node contains at least one + child that has documentation and is not private or internal. + */ +bool HeaderNode::hasDocumentedChildren() const +{ + return std::any_of(m_children.cbegin(), m_children.cend(), + [](Node *child) { return child->isInAPI(); }); +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/headernode.h b/src/qdoc/qdoc/src/qdoc/headernode.h new file mode 100644 index 000000000..b20ff8fdb --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/headernode.h @@ -0,0 +1,46 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef HEADERNODE_H +#define HEADERNODE_H + +#include "aggregate.h" + +#include <QtCore/qglobal.h> +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +class HeaderNode : public Aggregate +{ +public: + HeaderNode(Aggregate *parent, const QString &name); + [[nodiscard]] bool docMustBeGenerated() const override; + [[nodiscard]] bool isFirstClassAggregate() const override { return true; } + [[nodiscard]] bool isRelatableType() const override { return true; } + [[nodiscard]] QString title() const override { return (m_title.isEmpty() ? name() : m_title); } + [[nodiscard]] QString subtitle() const override { return m_subtitle; } + [[nodiscard]] QString fullTitle() const override + { + return (m_title.isEmpty() ? name() : name() + " - " + m_title); + } + bool setTitle(const QString &title) override + { + m_title = title; + return true; + } + bool setSubtitle(const QString &subtitle) override + { + m_subtitle = subtitle; + return true; + } + [[nodiscard]] bool hasDocumentedChildren() const; + +private: + QString m_title {}; + QString m_subtitle {}; +}; + +QT_END_NAMESPACE + +#endif // HEADERNODE_H diff --git a/src/qdoc/qdoc/src/qdoc/helpprojectwriter.cpp b/src/qdoc/qdoc/src/qdoc/helpprojectwriter.cpp new file mode 100644 index 000000000..968bb7b25 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/helpprojectwriter.cpp @@ -0,0 +1,769 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "helpprojectwriter.h" + +#include "access.h" +#include "aggregate.h" +#include "atom.h" +#include "classnode.h" +#include "collectionnode.h" +#include "config.h" +#include "enumnode.h" +#include "functionnode.h" +#include "htmlgenerator.h" +#include "node.h" +#include "qdocdatabase.h" +#include "typedefnode.h" + +#include <QtCore/qhash.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +HelpProjectWriter::HelpProjectWriter(const QString &defaultFileName, Generator *g) +{ + reset(defaultFileName, g); +} + +void HelpProjectWriter::reset(const QString &defaultFileName, Generator *g) +{ + m_projects.clear(); + m_gen = g; + /* + Get the pointer to the singleton for the qdoc database and + store it locally. This replaces all the local accesses to + the node tree, which are now private. + */ + m_qdb = QDocDatabase::qdocDB(); + + // The output directory should already have been checked by the calling + // generator. + Config &config = Config::instance(); + m_outputDir = config.getOutputDir(); + + const QStringList names{config.get(CONFIG_QHP + Config::dot + "projects").asStringList()}; + + for (const auto &projectName : names) { + HelpProject project; + project.m_name = projectName; + + QString prefix = CONFIG_QHP + Config::dot + projectName + Config::dot; + project.m_helpNamespace = config.get(prefix + "namespace").asString(); + project.m_virtualFolder = config.get(prefix + "virtualFolder").asString(); + project.m_version = config.get(CONFIG_VERSION).asString(); + project.m_fileName = config.get(prefix + "file").asString(); + if (project.m_fileName.isEmpty()) + project.m_fileName = defaultFileName; + project.m_extraFiles = config.get(prefix + "extraFiles").asStringSet(); + project.m_extraFiles += config.get(CONFIG_QHP + Config::dot + "extraFiles").asStringSet(); + project.m_indexTitle = config.get(prefix + "indexTitle").asString(); + project.m_indexRoot = config.get(prefix + "indexRoot").asString(); + project.m_filterAttributes = config.get(prefix + "filterAttributes").asStringSet(); + project.m_includeIndexNodes = config.get(prefix + "includeIndexNodes").asBool(); + const QSet<QString> customFilterNames = config.subVars(prefix + "customFilters"); + for (const auto &filterName : customFilterNames) { + QString name{config.get(prefix + "customFilters" + Config::dot + filterName + + Config::dot + "name").asString()}; + project.m_customFilters[name] = + config.get(prefix + "customFilters" + Config::dot + filterName + + Config::dot + "filterAttributes").asStringSet(); + } + + const auto excludedPrefixes = config.get(prefix + "excluded").asStringSet(); + for (auto name : excludedPrefixes) + project.m_excluded.insert(name.replace(QLatin1Char('\\'), QLatin1Char('/'))); + + const auto subprojectPrefixes{config.get(prefix + "subprojects").asStringList()}; + for (const auto &name : subprojectPrefixes) { + SubProject subproject; + QString subprefix = prefix + "subprojects" + Config::dot + name + Config::dot; + subproject.m_title = config.get(subprefix + "title").asString(); + if (subproject.m_title.isEmpty()) + continue; + subproject.m_indexTitle = config.get(subprefix + "indexTitle").asString(); + subproject.m_sortPages = config.get(subprefix + "sortPages").asBool(); + subproject.m_type = config.get(subprefix + "type").asString(); + readSelectors(subproject, config.get(subprefix + "selectors").asStringList()); + project.m_subprojects.append(subproject); + } + + if (project.m_subprojects.isEmpty()) { + SubProject subproject; + readSelectors(subproject, config.get(prefix + "selectors").asStringList()); + project.m_subprojects.insert(0, subproject); + } + + m_projects.append(project); + } +} + +void HelpProjectWriter::readSelectors(SubProject &subproject, const QStringList &selectors) +{ + QHash<QString, Node::NodeType> typeHash; + typeHash["namespace"] = Node::Namespace; + typeHash["class"] = Node::Class; + typeHash["struct"] = Node::Struct; + typeHash["union"] = Node::Union; + typeHash["header"] = Node::HeaderFile; + typeHash["headerfile"] = Node::HeaderFile; + typeHash["doc"] = Node::Page; // Unused (supported but ignored as a prefix) + typeHash["fake"] = Node::Page; // Unused (supported but ignored as a prefix) + typeHash["page"] = Node::Page; + typeHash["enum"] = Node::Enum; + typeHash["example"] = Node::Example; + typeHash["externalpage"] = Node::ExternalPage; + typeHash["typedef"] = Node::Typedef; + typeHash["typealias"] = Node::TypeAlias; + typeHash["function"] = Node::Function; + typeHash["property"] = Node::Property; + typeHash["variable"] = Node::Variable; + typeHash["group"] = Node::Group; + typeHash["module"] = Node::Module; + typeHash["qmlmodule"] = Node::QmlModule; + typeHash["qmlproperty"] = Node::QmlProperty; + typeHash["qmlclass"] = Node::QmlType; // Legacy alias for 'qmltype' + typeHash["qmltype"] = Node::QmlType; + typeHash["qmlbasictype"] = Node::QmlValueType; // Legacy alias for 'qmlvaluetype' + typeHash["qmlvaluetype"] = Node::QmlValueType; + + for (const QString &selector : selectors) { + QStringList pieces = selector.split(QLatin1Char(':')); + // Remove doc: or fake: prefix + if (pieces.size() > 1 && typeHash.value(pieces[0].toLower()) == Node::Page) + pieces.takeFirst(); + + QString typeName = pieces.takeFirst().toLower(); + if (!typeHash.contains(typeName)) + continue; + + subproject.m_selectors << typeHash.value(typeName); + if (!pieces.isEmpty()) { + pieces = pieces[0].split(QLatin1Char(',')); + for (const auto &piece : std::as_const(pieces)) { + if (typeHash[typeName] == Node::Group + || typeHash[typeName] == Node::Module + || typeHash[typeName] == Node::QmlModule) { + subproject.m_groups << piece.toLower(); + } + } + } + } +} + +void HelpProjectWriter::addExtraFile(const QString &file) +{ + for (HelpProject &project : m_projects) + project.m_extraFiles.insert(file); +} + +Keyword HelpProjectWriter::keywordDetails(const Node *node) const +{ + QString ref = m_gen->fullDocumentLocation(node); + + if (node->parent() && !node->parent()->name().isEmpty()) { + QString name = (node->isEnumType() || node->isTypedef()) + ? node->parent()->name()+"::"+node->name() + : node->name(); + QString id = (!node->isRelatedNonmember()) + ? node->parent()->name()+"::"+node->name() + : node->name(); + return Keyword(name, id, ref); + } else if (node->isQmlType()) { + const QString &name = node->name(); + QString moduleName = node->logicalModuleName(); + QStringList ids("QML." + name); + if (!moduleName.isEmpty()) { + QString majorVersion = node->logicalModule() + ? node->logicalModule()->logicalModuleVersion().split('.')[0] + : QString(); + ids << "QML." + moduleName + majorVersion + "." + name; + } + return Keyword(name, ids, ref); + } else if (node->isQmlModule()) { + const QLatin1Char delim('.'); + QStringList parts = node->logicalModuleName().split(delim) << "QML"; + std::reverse(parts.begin(), parts.end()); + return Keyword(node->logicalModuleName(), parts.join(delim), ref); + } else if (node->isTextPageNode()) { + const auto *pageNode = static_cast<const PageNode *>(node); + return Keyword(pageNode->fullTitle(), pageNode->fullTitle(), ref); + } else { + return Keyword(node->name(), node->name(), ref); + } +} + +bool HelpProjectWriter::generateSection(HelpProject &project, QXmlStreamWriter & /* writer */, + const Node *node) +{ + if (!node->url().isEmpty() && !(project.m_includeIndexNodes && !node->url().startsWith("http"))) + return false; + + if (node->isPrivate() || node->isInternal() || node->isDontDocument()) + return false; + + if (node->name().isEmpty()) + return true; + + QString docPath = node->doc().location().filePath(); + if (!docPath.isEmpty() && project.m_excluded.contains(docPath)) + return false; + + QString objName = node->isTextPageNode() ? node->fullTitle() : node->fullDocumentName(); + // Only add nodes to the set for each subproject if they match a selector. + // Those that match will be listed in the table of contents. + + for (int i = 0; i < project.m_subprojects.size(); i++) { + SubProject subproject = project.m_subprojects[i]; + // No selectors: accept all nodes. + if (subproject.m_selectors.isEmpty()) { + project.m_subprojects[i].m_nodes[objName] = node; + } else if (subproject.m_selectors.contains(node->nodeType())) { + // Add all group members for '[group|module|qmlmodule]:name' selector + if (node->isCollectionNode()) { + if (project.m_subprojects[i].m_groups.contains(node->name().toLower())) { + const auto *cn = static_cast<const CollectionNode *>(node); + const auto members = cn->members(); + for (const Node *m : members) { + if (!m->isInAPI()) + continue; + QString memberName = + m->isTextPageNode() ? m->fullTitle() : m->fullDocumentName(); + project.m_subprojects[i].m_nodes[memberName] = m; + } + continue; + } else if (!project.m_subprojects[i].m_groups.isEmpty()) { + continue; // Node does not represent specified group(s) + } + } else if (node->isTextPageNode()) { + if (node->isExternalPage() || node->fullTitle().isEmpty()) + continue; + } + project.m_subprojects[i].m_nodes[objName] = node; + } + } + + switch (node->nodeType()) { + + case Node::Class: + case Node::Struct: + case Node::Union: + project.m_keywords.append(keywordDetails(node)); + break; + case Node::QmlType: + case Node::QmlValueType: + if (node->doc().hasKeywords()) { + const auto keywords = node->doc().keywords(); + for (const Atom *keyword : keywords) { + if (!keyword->string().isEmpty()) { + project.m_keywords.append(Keyword(keyword->string(), keyword->string(), + m_gen->fullDocumentLocation(node))); + } + else + node->doc().location().warning( + QStringLiteral("Bad keyword in %1") + .arg(m_gen->fullDocumentLocation(node))); + } + } + project.m_keywords.append(keywordDetails(node)); + break; + + case Node::Namespace: + project.m_keywords.append(keywordDetails(node)); + break; + + case Node::Enum: + project.m_keywords.append(keywordDetails(node)); + { + const auto *enumNode = static_cast<const EnumNode *>(node); + const auto items = enumNode->items(); + for (const auto &item : items) { + if (enumNode->itemAccess(item.name()) == Access::Private) + continue; + + QString name; + QString id; + if (!node->parent()->name().isEmpty()) { + name = id = node->parent()->name() + "::" + item.name(); + } else { + name = id = item.name(); + } + QString ref = m_gen->fullDocumentLocation(node); + project.m_keywords.append(Keyword(name, id, ref)); + } + } + break; + + case Node::Group: + case Node::Module: + case Node::QmlModule: { + const auto *cn = static_cast<const CollectionNode *>(node); + if (!cn->fullTitle().isEmpty()) { + if (cn->doc().hasKeywords()) { + const auto keywords = cn->doc().keywords(); + for (const Atom *keyword : keywords) { + if (!keyword->string().isEmpty()) { + project.m_keywords.append( + Keyword(keyword->string(), keyword->string(), + m_gen->fullDocumentLocation(node))); + } else + cn->doc().location().warning( + QStringLiteral("Bad keyword in %1") + .arg(m_gen->fullDocumentLocation(node))); + } + } + project.m_keywords.append(keywordDetails(node)); + } + } break; + + case Node::Property: + case Node::QmlProperty: + project.m_keywords.append(keywordDetails(node)); + break; + + case Node::Function: { + const auto *funcNode = static_cast<const FunctionNode *>(node); + + /* + QML methods, signals, and signal handlers used to be node types, + but now they are Function nodes with a Metaness value that specifies + what kind of function they are, QmlSignal, QmlMethod, etc. + */ + if (funcNode->isQmlNode()) { + project.m_keywords.append(keywordDetails(node)); + break; + } + // Only insert keywords for non-constructors. Constructors are covered + // by the classes themselves. + + if (!funcNode->isSomeCtor()) + project.m_keywords.append(keywordDetails(node)); + + // Insert member status flags into the entries for the parent + // node of the function, or the node it is related to. + // Since parent nodes should have already been inserted into + // the set of files, we only need to ensure that related nodes + // are inserted. + + if (node->parent()) + project.m_memberStatus[node->parent()].insert(node->status()); + } break; + case Node::TypeAlias: + case Node::Typedef: { + const auto *typedefNode = static_cast<const TypedefNode *>(node); + Keyword typedefDetails = keywordDetails(node); + const EnumNode *enumNode = typedefNode->associatedEnum(); + // Use the location of any associated enum node in preference + // to that of the typedef. + if (enumNode) + typedefDetails.m_ref = m_gen->fullDocumentLocation(enumNode); + + project.m_keywords.append(typedefDetails); + } break; + + case Node::Variable: { + project.m_keywords.append(keywordDetails(node)); + } break; + + // Page nodes (such as manual pages) contain subtypes, titles and other + // attributes. + case Node::Page: { + const auto *pn = static_cast<const PageNode *>(node); + if (!pn->fullTitle().isEmpty()) { + if (pn->doc().hasKeywords()) { + const auto keywords = pn->doc().keywords(); + for (const Atom *keyword : keywords) { + if (!keyword->string().isEmpty()) { + project.m_keywords.append( + Keyword(keyword->string(), keyword->string(), + m_gen->fullDocumentLocation(node))); + } else { + QString loc = m_gen->fullDocumentLocation(node); + pn->doc().location().warning(QStringLiteral("Bad keyword in %1").arg(loc)); + } + } + } + project.m_keywords.append(keywordDetails(node)); + } + break; + } + default:; + } + + // Add all images referenced in the page to the set of files to include. + const Atom *atom = node->doc().body().firstAtom(); + while (atom) { + if (atom->type() == Atom::Image || atom->type() == Atom::InlineImage) { + // Images are all placed within a single directory regardless of + // whether the source images are in a nested directory structure. + QStringList pieces = atom->string().split(QLatin1Char('/')); + project.m_files.insert("images/" + pieces.last()); + } + atom = atom->next(); + } + + return true; +} + +void HelpProjectWriter::generateSections(HelpProject &project, QXmlStreamWriter &writer, + const Node *node) +{ + /* + Don't include index nodes in the help file. + */ + if (node->isIndexNode()) + return; + if (!generateSection(project, writer, node)) + return; + + if (node->isAggregate()) { + const auto *aggregate = static_cast<const Aggregate *>(node); + + // Ensure that we don't visit nodes more than once. + NodeList childSet; + NodeList children = aggregate->childNodes(); + std::sort(children.begin(), children.end(), Node::nodeNameLessThan); + for (auto *child : children) { + // Skip related non-members adopted by some other aggregate + if (child->parent() != aggregate) + continue; + if (child->isIndexNode() || child->isPrivate()) + continue; + if (child->isTextPageNode()) { + if (!childSet.contains(child)) + childSet << child; + } else { + // Store member status of children + project.m_memberStatus[node].insert(child->status()); + if (child->isFunction() && static_cast<const FunctionNode *>(child)->isOverload()) + continue; + if (!childSet.contains(child)) + childSet << child; + } + } + for (const auto *child : std::as_const(childSet)) + generateSections(project, writer, child); + } +} + +void HelpProjectWriter::generate() +{ + // Warn if .qhp configuration was expected but not provided + if (auto &config = Config::instance(); m_projects.isEmpty() && config.get(CONFIG_QHP).asBool()) { + config.location().warning(u"Documentation configuration for '%1' doesn't define a help project (qhp)"_s + .arg(config.get(CONFIG_PROJECT).asString())); + } + for (HelpProject &project : m_projects) + generateProject(project); +} + +void HelpProjectWriter::writeSection(QXmlStreamWriter &writer, const QString &path, + const QString &value) +{ + writer.writeStartElement(QStringLiteral("section")); + writer.writeAttribute(QStringLiteral("ref"), path); + writer.writeAttribute(QStringLiteral("title"), value); + writer.writeEndElement(); // section +} + +/*! + Write subsections for all members, compatibility members and obsolete members. + */ +void HelpProjectWriter::addMembers(HelpProject &project, QXmlStreamWriter &writer, const Node *node) +{ + QString href = m_gen->fullDocumentLocation(node); + href = href.left(href.size() - 5); + if (href.isEmpty()) + return; + + bool derivedClass = false; + if (node->isClassNode()) + derivedClass = !(static_cast<const ClassNode *>(node)->baseClasses().isEmpty()); + + // Do not generate a 'List of all members' for namespaces or header files, + // but always generate it for derived classes and QML types (but not QML value types) + if (!node->isNamespace() && !node->isHeader() && !node->isQmlBasicType() + && (derivedClass || node->isQmlType() || !project.m_memberStatus[node].isEmpty())) { + QString membersPath = href + QStringLiteral("-members.html"); + writeSection(writer, membersPath, QStringLiteral("List of all members")); + } + if (project.m_memberStatus[node].contains(Node::Deprecated)) { + QString obsoletePath = href + QStringLiteral("-obsolete.html"); + writeSection(writer, obsoletePath, QStringLiteral("Obsolete members")); + } +} + +void HelpProjectWriter::writeNode(HelpProject &project, QXmlStreamWriter &writer, const Node *node) +{ + QString href = m_gen->fullDocumentLocation(node); + QString objName = node->name(); + + switch (node->nodeType()) { + + case Node::Class: + case Node::Struct: + case Node::Union: + case Node::QmlType: + case Node::QmlValueType: { + QString typeStr = m_gen->typeString(node); + if (!typeStr.isEmpty()) + typeStr[0] = typeStr[0].toTitleCase(); + writer.writeStartElement("section"); + writer.writeAttribute("ref", href); + if (node->parent() && !node->parent()->name().isEmpty()) + writer.writeAttribute("title", + QStringLiteral("%1::%2 %3 Reference") + .arg(node->parent()->name(), objName, typeStr)); + else + writer.writeAttribute("title", QStringLiteral("%1 %2 Reference").arg(objName, typeStr)); + + addMembers(project, writer, node); + writer.writeEndElement(); // section + } break; + + case Node::Namespace: + writeSection(writer, href, objName); + break; + + case Node::Example: + case Node::HeaderFile: + case Node::Page: + case Node::Group: + case Node::Module: + case Node::QmlModule: { + writer.writeStartElement("section"); + writer.writeAttribute("ref", href); + writer.writeAttribute("title", node->fullTitle()); + if (node->nodeType() == Node::HeaderFile) + addMembers(project, writer, node); + writer.writeEndElement(); // section + } break; + default:; + } +} + +void HelpProjectWriter::generateProject(HelpProject &project) +{ + const Node *rootNode; + + // Restrict searching only to the local (primary) tree + QList<Tree *> searchOrder = m_qdb->searchOrder(); + m_qdb->setLocalSearch(); + + if (!project.m_indexRoot.isEmpty()) + rootNode = m_qdb->findPageNodeByTitle(project.m_indexRoot); + else + rootNode = m_qdb->primaryTreeRoot(); + + if (rootNode == nullptr) + return; + + project.m_files.clear(); + project.m_keywords.clear(); + + QFile file(m_outputDir + QDir::separator() + project.m_fileName); + if (!file.open(QFile::WriteOnly)) + return; + + QXmlStreamWriter writer(&file); + writer.setAutoFormatting(true); + writer.writeStartDocument(); + writer.writeStartElement("QtHelpProject"); + writer.writeAttribute("version", "1.0"); + + // Write metaData, virtualFolder and namespace elements. + writer.writeTextElement("namespace", project.m_helpNamespace); + writer.writeTextElement("virtualFolder", project.m_virtualFolder); + writer.writeStartElement("metaData"); + writer.writeAttribute("name", "version"); + writer.writeAttribute("value", project.m_version); + writer.writeEndElement(); + + // Write customFilter elements. + for (auto it = project.m_customFilters.constBegin(); it != project.m_customFilters.constEnd(); + ++it) { + writer.writeStartElement("customFilter"); + writer.writeAttribute("name", it.key()); + QStringList sortedAttributes = it.value().values(); + sortedAttributes.sort(); + for (const auto &filter : std::as_const(sortedAttributes)) + writer.writeTextElement("filterAttribute", filter); + writer.writeEndElement(); // customFilter + } + + // Start the filterSection. + writer.writeStartElement("filterSection"); + + // Write filterAttribute elements. + QStringList sortedFilterAttributes = project.m_filterAttributes.values(); + sortedFilterAttributes.sort(); + for (const auto &filterName : std::as_const(sortedFilterAttributes)) + writer.writeTextElement("filterAttribute", filterName); + + writer.writeStartElement("toc"); + writer.writeStartElement("section"); + const Node *node = m_qdb->findPageNodeByTitle(project.m_indexTitle); + if (!node) + node = m_qdb->findNodeByNameAndType(QStringList(project.m_indexTitle), &Node::isPageNode); + if (!node) + node = m_qdb->findNodeByNameAndType(QStringList("index.html"), &Node::isPageNode); + QString indexPath; + if (node) + indexPath = m_gen->fullDocumentLocation(node); + else + indexPath = "index.html"; + writer.writeAttribute("ref", indexPath); + writer.writeAttribute("title", project.m_indexTitle); + + generateSections(project, writer, rootNode); + + for (int i = 0; i < project.m_subprojects.size(); i++) { + SubProject subproject = project.m_subprojects[i]; + + if (subproject.m_type == QLatin1String("manual")) { + + const Node *indexPage = m_qdb->findNodeForTarget(subproject.m_indexTitle, nullptr); + if (indexPage) { + Text indexBody = indexPage->doc().body(); + const Atom *atom = indexBody.firstAtom(); + QStack<int> sectionStack; + bool inItem = false; + + while (atom) { + switch (atom->type()) { + case Atom::ListLeft: + sectionStack.push(0); + break; + case Atom::ListRight: + if (sectionStack.pop() > 0) + writer.writeEndElement(); // section + break; + case Atom::ListItemLeft: + inItem = true; + break; + case Atom::ListItemRight: + inItem = false; + break; + case Atom::Link: + if (inItem) { + if (sectionStack.top() > 0) + writer.writeEndElement(); // section + + const Node *page = m_qdb->findNodeForTarget(atom->string(), nullptr); + writer.writeStartElement("section"); + QString indexPath = m_gen->fullDocumentLocation(page); + writer.writeAttribute("ref", indexPath); + writer.writeAttribute("title", atom->linkText()); + + sectionStack.top() += 1; + } + break; + default:; + } + + if (atom == indexBody.lastAtom()) + break; + atom = atom->next(); + } + } else + rootNode->doc().location().warning( + QStringLiteral("Failed to find index: %1").arg(subproject.m_indexTitle)); + + } else { + + writer.writeStartElement("section"); + QString indexPath = m_gen->fullDocumentLocation( + m_qdb->findNodeForTarget(subproject.m_indexTitle, nullptr)); + writer.writeAttribute("ref", indexPath); + writer.writeAttribute("title", subproject.m_title); + + if (subproject.m_sortPages) { + QStringList titles = subproject.m_nodes.keys(); + titles.sort(); + for (const auto &title : std::as_const(titles)) { + writeNode(project, writer, subproject.m_nodes[title]); + } + } else { + // Find a contents node and navigate from there, using the NextLink values. + QSet<QString> visited; + bool contentsFound = false; + for (const auto *node : std::as_const(subproject.m_nodes)) { + QString nextTitle = node->links().value(Node::NextLink).first; + if (!nextTitle.isEmpty() + && node->links().value(Node::ContentsLink).first.isEmpty()) { + + const Node *nextPage = m_qdb->findNodeForTarget(nextTitle, nullptr); + + // Write the contents node. + writeNode(project, writer, node); + contentsFound = true; + + while (nextPage) { + writeNode(project, writer, nextPage); + nextTitle = nextPage->links().value(Node::NextLink).first; + if (nextTitle.isEmpty() || visited.contains(nextTitle)) + break; + nextPage = m_qdb->findNodeForTarget(nextTitle, nullptr); + visited.insert(nextTitle); + } + break; + } + } + // No contents/nextpage links found, write all nodes unsorted + if (!contentsFound) { + QList<const Node *> subnodes = subproject.m_nodes.values(); + + std::sort(subnodes.begin(), subnodes.end(), Node::nodeNameLessThan); + + for (const auto *node : std::as_const(subnodes)) + writeNode(project, writer, node); + } + } + + writer.writeEndElement(); // section + } + } + + // Restore original search order + m_qdb->setSearchOrder(searchOrder); + + writer.writeEndElement(); // section + writer.writeEndElement(); // toc + + writer.writeStartElement("keywords"); + std::sort(project.m_keywords.begin(), project.m_keywords.end()); + for (const auto &k : std::as_const(project.m_keywords)) { + for (const auto &id : std::as_const(k.m_ids)) { + writer.writeStartElement("keyword"); + writer.writeAttribute("name", k.m_name); + writer.writeAttribute("id", id); + writer.writeAttribute("ref", k.m_ref); + writer.writeEndElement(); //keyword + } + } + writer.writeEndElement(); // keywords + + writer.writeStartElement("files"); + + // The list of files to write is the union of generated files and + // other files (images and extras) included in the project + QSet<QString> files = + QSet<QString>(m_gen->outputFileNames().cbegin(), m_gen->outputFileNames().cend()); + files.unite(project.m_files); + files.unite(project.m_extraFiles); + QStringList sortedFiles = files.values(); + sortedFiles.sort(); + for (const auto &usedFile : std::as_const(sortedFiles)) { + if (!usedFile.isEmpty()) + writer.writeTextElement("file", usedFile); + } + writer.writeEndElement(); // files + + writer.writeEndElement(); // filterSection + writer.writeEndElement(); // QtHelpProject + writer.writeEndDocument(); + file.close(); +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/helpprojectwriter.h b/src/qdoc/qdoc/src/qdoc/helpprojectwriter.h new file mode 100644 index 000000000..11dd67fb1 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/helpprojectwriter.h @@ -0,0 +1,106 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef HELPPROJECTWRITER_H +#define HELPPROJECTWRITER_H + +#include "node.h" + +#include <QtCore/qstring.h> +#include <QtCore/qxmlstream.h> + +#include <utility> + +QT_BEGIN_NAMESPACE + +class QDocDatabase; +class Generator; + +using NodeTypeSet = QSet<unsigned char>; + +struct SubProject +{ + QString m_title {}; + QString m_indexTitle {}; + NodeTypeSet m_selectors {}; + bool m_sortPages {}; + QString m_type {}; + QHash<QString, const Node *> m_nodes {}; + QStringList m_groups {}; +}; + +/* + * Name is the human-readable name to be shown in Assistant. + * Ids is a list of unique identifiers. + * Ref is the location of the documentation for the keyword. + */ +struct Keyword { + QString m_name {}; + QStringList m_ids {}; + QString m_ref {}; + Keyword(QString name, const QString &id, QString ref) + : m_name(std::move(name)), m_ids(QStringList(id)), m_ref(std::move(ref)) + { + } + Keyword(QString name, QStringList ids, QString ref) + : m_name(std::move(name)), m_ids(std::move(ids)), m_ref(std::move(ref)) + { + } + bool operator<(const Keyword &o) const + { + // Order by name; use ref as a secondary sort key + return (m_name == o.m_name) ? m_ref < o.m_ref : m_name < o.m_name; + } +}; + +struct HelpProject +{ + using NodeStatusSet = QSet<unsigned char>; + + QString m_name {}; + QString m_helpNamespace {}; + QString m_virtualFolder {}; + QString m_version {}; + QString m_fileName {}; + QString m_indexRoot {}; + QString m_indexTitle {}; + QList<Keyword> m_keywords {}; + QSet<QString> m_files {}; + QSet<QString> m_extraFiles {}; + QSet<QString> m_filterAttributes {}; + QHash<QString, QSet<QString>> m_customFilters {}; + QSet<QString> m_excluded {}; + QList<SubProject> m_subprojects {}; + QHash<const Node *, NodeStatusSet> m_memberStatus {}; + bool m_includeIndexNodes {}; +}; + + +class HelpProjectWriter +{ +public: + HelpProjectWriter(const QString &defaultFileName, Generator *g); + void reset(const QString &defaultFileName, Generator *g); + void addExtraFile(const QString &file); + void generate(); + +private: + void generateProject(HelpProject &project); + void generateSections(HelpProject &project, QXmlStreamWriter &writer, const Node *node); + bool generateSection(HelpProject &project, QXmlStreamWriter &writer, const Node *node); + Keyword keywordDetails(const Node *node) const; + void writeNode(HelpProject &project, QXmlStreamWriter &writer, const Node *node); + void readSelectors(SubProject &subproject, const QStringList &selectors); + void addMembers(HelpProject &project, QXmlStreamWriter &writer, const Node *node); + void writeSection(QXmlStreamWriter &writer, const QString &path, const QString &value); + + QDocDatabase *m_qdb {}; + Generator *m_gen {}; + + QString m_outputDir {}; + QList<HelpProject> m_projects {}; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/htmlgenerator.cpp b/src/qdoc/qdoc/src/qdoc/htmlgenerator.cpp new file mode 100644 index 000000000..e18cac8b6 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/htmlgenerator.cpp @@ -0,0 +1,3733 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "htmlgenerator.h" + +#include "access.h" +#include "aggregate.h" +#include "classnode.h" +#include "collectionnode.h" +#include "config.h" +#include "codemarker.h" +#include "codeparser.h" +#include "enumnode.h" +#include "functionnode.h" +#include "helpprojectwriter.h" +#include "manifestwriter.h" +#include "node.h" +#include "propertynode.h" +#include "qdocdatabase.h" +#include "qmlpropertynode.h" +#include "sharedcommentnode.h" +#include "tagfilewriter.h" +#include "tree.h" +#include "quoter.h" +#include "utilities.h" + +#include <QtCore/qlist.h> +#include <QtCore/qmap.h> +#include <QtCore/quuid.h> +#include <QtCore/qversionnumber.h> +#include <QtCore/qregularexpression.h> + +#include <cctype> +#include <deque> +#include <string> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +bool HtmlGenerator::s_inUnorderedList { false }; + +HtmlGenerator::HtmlGenerator(FileResolver& file_resolver) : XmlGenerator(file_resolver) {} + +static void addLink(const QString &linkTarget, QStringView nestedStuff, QString *res) +{ + if (!linkTarget.isEmpty()) { + *res += QLatin1String("<a href=\""); + *res += linkTarget; + *res += QLatin1String("\" translate=\"no\">"); + *res += nestedStuff; + *res += QLatin1String("</a>"); + } else { + *res += nestedStuff; + } +} + +/*! + \internal + Convenience method that starts an unordered list if not in one. + */ +inline void HtmlGenerator::openUnorderedList() +{ + if (!s_inUnorderedList) { + out() << "<ul>\n"; + s_inUnorderedList = true; + } +} + +/*! + \internal + Convenience method that closes an unordered list if in one. + */ +inline void HtmlGenerator::closeUnorderedList() +{ + if (s_inUnorderedList) { + out() << "</ul>\n"; + s_inUnorderedList = false; + } +} + +/*! + Destroys the HTML output generator. Deletes the singleton + instance of HelpProjectWriter and the ManifestWriter instance. + */ +HtmlGenerator::~HtmlGenerator() +{ + if (m_helpProjectWriter) { + delete m_helpProjectWriter; + m_helpProjectWriter = nullptr; + } + + if (m_manifestWriter) { + delete m_manifestWriter; + m_manifestWriter = nullptr; + } +} + +/*! + Initializes the HTML output generator's data structures + from the configuration (Config) singleton. + */ +void HtmlGenerator::initializeGenerator() +{ + static const struct + { + const char *key; + const char *left; + const char *right; + } defaults[] = { { ATOM_FORMATTING_BOLD, "<b>", "</b>" }, + { ATOM_FORMATTING_INDEX, "<!--", "-->" }, + { ATOM_FORMATTING_ITALIC, "<i>", "</i>" }, + { ATOM_FORMATTING_PARAMETER, "<i translate=\"no\">", "</i>" }, + { ATOM_FORMATTING_SUBSCRIPT, "<sub>", "</sub>" }, + { ATOM_FORMATTING_SUPERSCRIPT, "<sup>", "</sup>" }, + { ATOM_FORMATTING_TELETYPE, "<code translate=\"no\">", + "</code>" }, // <tt> tag is not supported in HTML5 + { ATOM_FORMATTING_TRADEMARK, "<span translate=\"no\">", "™" }, + { ATOM_FORMATTING_UICONTROL, "<b translate=\"no\">", "</b>" }, + { ATOM_FORMATTING_UNDERLINE, "<u>", "</u>" }, + { nullptr, nullptr, nullptr } }; + + Generator::initializeGenerator(); + config = &Config::instance(); + + /* + The formatting maps are owned by Generator. They are cleared in + Generator::terminate(). + */ + for (int i = 0; defaults[i].key; ++i) { + formattingLeftMap().insert(QLatin1String(defaults[i].key), QLatin1String(defaults[i].left)); + formattingRightMap().insert(QLatin1String(defaults[i].key), + QLatin1String(defaults[i].right)); + } + + QString formatDot{HtmlGenerator::format() + Config::dot}; + m_endHeader = config->get(formatDot + CONFIG_ENDHEADER).asString(); + m_postHeader = config->get(formatDot + HTMLGENERATOR_POSTHEADER).asString(); + m_postPostHeader = config->get(formatDot + HTMLGENERATOR_POSTPOSTHEADER).asString(); + m_prologue = config->get(formatDot + HTMLGENERATOR_PROLOGUE).asString(); + + m_footer = config->get(formatDot + HTMLGENERATOR_FOOTER).asString(); + m_address = config->get(formatDot + HTMLGENERATOR_ADDRESS).asString(); + m_noNavigationBar = config->get(formatDot + HTMLGENERATOR_NONAVIGATIONBAR).asBool(); + m_navigationSeparator = config->get(formatDot + HTMLGENERATOR_NAVIGATIONSEPARATOR).asString(); + tocDepth = config->get(formatDot + HTMLGENERATOR_TOCDEPTH).asInt(); + + m_project = config->get(CONFIG_PROJECT).asString(); + m_projectDescription = config->get(CONFIG_DESCRIPTION) + .asString(m_project + QLatin1String(" Reference Documentation")); + + m_projectUrl = config->get(CONFIG_URL).asString(); + tagFile_ = config->get(CONFIG_TAGFILE).asString(); + naturalLanguage = config->get(CONFIG_NATURALLANGUAGE).asString(QLatin1String("en")); + + m_codeIndent = config->get(CONFIG_CODEINDENT).asInt(); + m_codePrefix = config->get(CONFIG_CODEPREFIX).asString(); + m_codeSuffix = config->get(CONFIG_CODESUFFIX).asString(); + + /* + The help file write should be allocated once and only once + per qdoc execution. + */ + if (m_helpProjectWriter) + m_helpProjectWriter->reset(m_project.toLower() + ".qhp", this); + else + m_helpProjectWriter = new HelpProjectWriter(m_project.toLower() + ".qhp", this); + + if (!m_manifestWriter) + m_manifestWriter = new ManifestWriter(); + + // Documentation template handling + m_headerScripts = config->get(formatDot + CONFIG_HEADERSCRIPTS).asString(); + m_headerStyles = config->get(formatDot + CONFIG_HEADERSTYLES).asString(); + + // Retrieve the config for the navigation bar + m_homepage = config->get(CONFIG_NAVIGATION + + Config::dot + CONFIG_HOMEPAGE).asString(); + + m_hometitle = config->get(CONFIG_NAVIGATION + + Config::dot + CONFIG_HOMETITLE) + .asString(m_homepage); + + m_landingpage = config->get(CONFIG_NAVIGATION + + Config::dot + CONFIG_LANDINGPAGE).asString(); + + m_landingtitle = config->get(CONFIG_NAVIGATION + + Config::dot + CONFIG_LANDINGTITLE) + .asString(m_landingpage); + + m_cppclassespage = config->get(CONFIG_NAVIGATION + + Config::dot + CONFIG_CPPCLASSESPAGE).asString(); + + m_cppclassestitle = config->get(CONFIG_NAVIGATION + + Config::dot + CONFIG_CPPCLASSESTITLE) + .asString(QLatin1String("C++ Classes")); + + m_qmltypespage = config->get(CONFIG_NAVIGATION + + Config::dot + CONFIG_QMLTYPESPAGE).asString(); + + m_qmltypestitle = config->get(CONFIG_NAVIGATION + + Config::dot + CONFIG_QMLTYPESTITLE) + .asString(QLatin1String("QML Types")); + + m_trademarkspage = config->get(CONFIG_NAVIGATION + + Config::dot + CONFIG_TRADEMARKSPAGE).asString(); + + m_buildversion = config->get(CONFIG_BUILDVERSION).asString(); +} + +/*! + Gracefully terminates the HTML output generator. + */ +void HtmlGenerator::terminateGenerator() +{ + Generator::terminateGenerator(); +} + +QString HtmlGenerator::format() +{ + return "HTML"; +} + +/*! + If qdoc is in the \c {-prepare} phase, traverse the primary + tree to generate the index file for the current module. + + If qdoc is in the \c {-generate} phase, traverse the primary + tree to generate all the HTML documentation for the current + module. Then generate the help file and the tag file. + */ +void HtmlGenerator::generateDocs() +{ + Node *qflags = m_qdb->findClassNode(QStringList("QFlags")); + if (qflags) + m_qflagsHref = linkForNode(qflags, nullptr); + if (!config->preparing()) + Generator::generateDocs(); + + if (!config->generating()) { + QString fileBase = + m_project.toLower().simplified().replace(QLatin1Char(' '), QLatin1Char('-')); + m_qdb->generateIndex(outputDir() + QLatin1Char('/') + fileBase + ".index", m_projectUrl, + m_projectDescription, this); + } + + if (!config->preparing()) { + m_helpProjectWriter->generate(); + m_manifestWriter->generateManifestFiles(); + /* + Generate the XML tag file, if it was requested. + */ + if (!tagFile_.isEmpty()) { + TagFileWriter tagFileWriter; + tagFileWriter.generateTagFile(tagFile_, this); + } + } +} + +/*! + Generate an html file with the contents of a C++ or QML source file. + */ +void HtmlGenerator::generateExampleFilePage(const Node *en, ResolvedFile resolved_file, CodeMarker *marker) +{ + SubTitleSize subTitleSize = LargeSubTitle; + QString fullTitle = en->fullTitle(); + + beginSubPage(en, linkForExampleFile(resolved_file.get_query())); + generateHeader(fullTitle, en, marker); + generateTitle(fullTitle, Text() << en->subtitle(), subTitleSize, en, marker); + + Text text; + Quoter quoter; + Doc::quoteFromFile(en->doc().location(), quoter, resolved_file); + QString code = quoter.quoteTo(en->location(), QString(), QString()); + CodeMarker *codeMarker = CodeMarker::markerForFileName(resolved_file.get_path()); + text << Atom(codeMarker->atomType(), code); + Atom a(codeMarker->atomType(), code); + + generateText(text, en, codeMarker); + endSubPage(); +} + +/*! + Generate html from an instance of Atom. + */ +qsizetype HtmlGenerator::generateAtom(const Atom *atom, const Node *relative, CodeMarker *marker) +{ + qsizetype idx, skipAhead = 0; + static bool in_para = false; + Node::Genus genus = Node::DontCare; + + switch (atom->type()) { + case Atom::AutoLink: { + QString name = atom->string(); + if (relative && relative->name() == name.replace(QLatin1String("()"), QLatin1String())) { + out() << protectEnc(atom->string()); + break; + } + // Allow auto-linking to nodes in API reference + genus = Node::API; + } + Q_FALLTHROUGH(); + case Atom::NavAutoLink: + if (!m_inLink && !m_inContents && !m_inSectionHeading) { + const Node *node = nullptr; + QString link = getAutoLink(atom, relative, &node, genus); + if (link.isEmpty()) { + if (autolinkErrors() && relative) + relative->doc().location().warning( + QStringLiteral("Can't autolink to '%1'").arg(atom->string())); + } else if (node && node->isDeprecated()) { + if (relative && (relative->parent() != node) && !relative->isDeprecated()) + link.clear(); + } + if (link.isEmpty()) { + out() << protectEnc(atom->string()); + } else { + beginLink(link, node, relative); + generateLink(atom); + endLink(); + } + } else { + out() << protectEnc(atom->string()); + } + break; + case Atom::BaseName: + break; + case Atom::BriefLeft: + if (!hasBrief(relative)) { + skipAhead = skipAtoms(atom, Atom::BriefRight); + break; + } + out() << "<p>"; + rewritePropertyBrief(atom, relative); + break; + case Atom::BriefRight: + if (hasBrief(relative)) + out() << "</p>\n"; + break; + case Atom::C: + // This may at one time have been used to mark up C++ code but it is + // now widely used to write teletype text. As a result, text marked + // with the \c command is not passed to a code marker. + out() << formattingLeftMap()[ATOM_FORMATTING_TELETYPE]; + out() << protectEnc(plainCode(atom->string())); + out() << formattingRightMap()[ATOM_FORMATTING_TELETYPE]; + break; + case Atom::CaptionLeft: + out() << "<p class=\"figCaption\">"; + in_para = true; + break; + case Atom::CaptionRight: + endLink(); + if (in_para) { + out() << "</p>\n"; + in_para = false; + } + break; + case Atom::Qml: + out() << "<pre class=\"qml\" translate=\"no\">" + << trimmedTrailing(highlightedCode(indent(m_codeIndent, atom->string()), relative, + false, Node::QML), + m_codePrefix, m_codeSuffix) + << "</pre>\n"; + break; + case Atom::Code: + out() << "<pre class=\"cpp\" translate=\"no\">" + << trimmedTrailing(highlightedCode(indent(m_codeIndent, atom->string()), relative), + m_codePrefix, m_codeSuffix) + << "</pre>\n"; + break; + case Atom::CodeBad: + out() << "<pre class=\"cpp plain\" translate=\"no\">" + << trimmedTrailing(protectEnc(plainCode(indent(m_codeIndent, atom->string()))), + m_codePrefix, m_codeSuffix) + << "</pre>\n"; + break; + case Atom::DetailsLeft: + out() << "<details>\n"; + if (!atom->string().isEmpty()) + out() << "<summary>" << protectEnc(atom->string()) << "</summary>\n"; + else + out() << "<summary>...</summary>\n"; + break; + case Atom::DetailsRight: + out() << "</details>\n"; + break; + case Atom::DivLeft: + out() << "<div"; + if (!atom->string().isEmpty()) + out() << ' ' << atom->string(); + out() << '>'; + break; + case Atom::DivRight: + out() << "</div>"; + break; + case Atom::FootnoteLeft: + // ### For now + if (in_para) { + out() << "</p>\n"; + in_para = false; + } + out() << "<!-- "; + break; + case Atom::FootnoteRight: + // ### For now + out() << "-->\n"; + break; + case Atom::FormatElse: + case Atom::FormatEndif: + case Atom::FormatIf: + break; + case Atom::FormattingLeft: + if (atom->string().startsWith("span ")) + out() << '<' + atom->string() << '>'; + else + out() << formattingLeftMap()[atom->string()]; + break; + case Atom::FormattingRight: + if (atom->string() == ATOM_FORMATTING_LINK) { + endLink(); + } else if (atom->string() == ATOM_FORMATTING_TRADEMARK) { + if (appendTrademark(atom)) { + // Make the trademark symbol a link to navigation.trademarkspage (if set) + const Node *node{nullptr}; + const Atom tm_link(Atom::NavLink, m_trademarkspage); + if (const auto &link = getLink(&tm_link, relative, &node); + !link.isEmpty() && node != relative) + out() << "<a href=\"%1\">%2</a>"_L1.arg(link, formattingRightMap()[atom->string()]); + else + out() << formattingRightMap()[atom->string()]; + } + out() << "</span>"; + } else if (atom->string().startsWith("span ")) { + out() << "</span>"; + } else { + out() << formattingRightMap()[atom->string()]; + } + break; + case Atom::AnnotatedList: { + if (const auto *cn = m_qdb->getCollectionNode(atom->string(), Node::Group); cn) + generateList(cn, marker, atom->string(), Generator::sortOrder(atom->strings().last())); + } break; + case Atom::GeneratedList: { + const auto sortOrder{Generator::sortOrder(atom->strings().last())}; + if (atom->string() == QLatin1String("annotatedclasses")) { + generateAnnotatedList(relative, marker, m_qdb->getCppClasses().values(), sortOrder); + } else if (atom->string() == QLatin1String("annotatedexamples")) { + generateAnnotatedLists(relative, marker, m_qdb->getExamples()); + } else if (atom->string() == QLatin1String("annotatedattributions")) { + generateAnnotatedLists(relative, marker, m_qdb->getAttributions()); + } else if (atom->string() == QLatin1String("classes")) { + generateCompactList(Generic, relative, m_qdb->getCppClasses(), true, + QStringLiteral("")); + } else if (atom->string().contains("classes ")) { + QString rootName = atom->string().mid(atom->string().indexOf("classes") + 7).trimmed(); + generateCompactList(Generic, relative, m_qdb->getCppClasses(), true, rootName); + } else if (atom->string() == QLatin1String("qmlvaluetypes") + || atom->string() == QLatin1String("qmlbasictypes")) { + generateCompactList(Generic, relative, m_qdb->getQmlValueTypes(), true, + QStringLiteral("")); + } else if (atom->string() == QLatin1String("qmltypes")) { + generateCompactList(Generic, relative, m_qdb->getQmlTypes(), true, QStringLiteral("")); + } else if ((idx = atom->string().indexOf(QStringLiteral("bymodule"))) != -1) { + QDocDatabase *qdb = QDocDatabase::qdocDB(); + QString moduleName = atom->string().mid(idx + 8).trimmed(); + Node::NodeType moduleType = typeFromString(atom); + if (const auto *cn = qdb->getCollectionNode(moduleName, moduleType)) { + NodeMap map; + switch (moduleType) { + case Node::Module: + // classesbymodule <module_name> + map = cn->getMembers([](const Node *n) { return n->isClassNode(); }); + generateAnnotatedList(relative, marker, map.values(), sortOrder); + break; + case Node::QmlModule: + if (atom->string().contains(QLatin1String("qmlvaluetypes"))) + map = cn->getMembers(Node::QmlValueType); // qmlvaluetypesbymodule <module_name> + else + map = cn->getMembers(Node::QmlType); // qmltypesbymodule <module_name> + generateAnnotatedList(relative, marker, map.values(), sortOrder); + break; + default: // fall back to listing all members + generateAnnotatedList(relative, marker, cn->members(), sortOrder); + break; + } + } + } else if (atom->string() == QLatin1String("classhierarchy")) { + generateClassHierarchy(relative, m_qdb->getCppClasses()); + } else if (atom->string() == QLatin1String("obsoleteclasses")) { + generateCompactList(Generic, relative, m_qdb->getObsoleteClasses(), false, + QStringLiteral("Q")); + } else if (atom->string() == QLatin1String("obsoleteqmltypes")) { + generateCompactList(Generic, relative, m_qdb->getObsoleteQmlTypes(), false, + QStringLiteral("")); + } else if (atom->string() == QLatin1String("obsoletecppmembers")) { + generateCompactList(Obsolete, relative, m_qdb->getClassesWithObsoleteMembers(), false, + QStringLiteral("Q")); + } else if (atom->string() == QLatin1String("obsoleteqmlmembers")) { + generateCompactList(Obsolete, relative, m_qdb->getQmlTypesWithObsoleteMembers(), false, + QStringLiteral("")); + } else if (atom->string() == QLatin1String("functionindex")) { + generateFunctionIndex(relative); + } else if (atom->string() == QLatin1String("attributions")) { + generateAnnotatedList(relative, marker, m_qdb->getAttributions().values(), sortOrder); + } else if (atom->string() == QLatin1String("legalese")) { + generateLegaleseList(relative, marker); + } else if (atom->string() == QLatin1String("overviews")) { + generateList(relative, marker, "overviews", sortOrder); + } else if (atom->string() == QLatin1String("cpp-modules")) { + generateList(relative, marker, "cpp-modules", sortOrder); + } else if (atom->string() == QLatin1String("qml-modules")) { + generateList(relative, marker, "qml-modules", sortOrder); + } else if (atom->string() == QLatin1String("namespaces")) { + generateAnnotatedList(relative, marker, m_qdb->getNamespaces().values(), sortOrder); + } else if (atom->string() == QLatin1String("related")) { + generateList(relative, marker, "related", sortOrder); + } else { + const CollectionNode *cn = m_qdb->getCollectionNode(atom->string(), Node::Group); + if (cn) { + if (!generateGroupList(const_cast<CollectionNode *>(cn), sortOrder)) + relative->location().warning( + QString("'\\generatelist %1' group is empty").arg(atom->string())); + } else { + relative->location().warning( + QString("'\\generatelist %1' no such group").arg(atom->string())); + } + } + } break; + case Atom::SinceList: { + const NodeMultiMap &nsmap = m_qdb->getSinceMap(atom->string()); + if (nsmap.isEmpty()) + break; + + const NodeMultiMap &ncmap = m_qdb->getClassMap(atom->string()); + const NodeMultiMap &nqcmap = m_qdb->getQmlTypeMap(atom->string()); + + Sections sections(nsmap); + out() << "<ul>\n"; + const QList<Section> sinceSections = sections.sinceSections(); + for (const auto §ion : sinceSections) { + if (!section.members().isEmpty()) { + out() << "<li>" + << "<a href=\"#" << Utilities::asAsciiPrintable(section.title()) << "\">" + << section.title() << "</a></li>\n"; + } + } + out() << "</ul>\n"; + + int index = 0; + for (const auto §ion : sinceSections) { + if (!section.members().isEmpty()) { + out() << "<h3 id=\"" << Utilities::asAsciiPrintable(section.title()) << "\">" + << protectEnc(section.title()) << "</h3>\n"; + if (index == Sections::SinceClasses) + generateCompactList(Generic, relative, ncmap, false, QStringLiteral("Q")); + else if (index == Sections::SinceQmlTypes) + generateCompactList(Generic, relative, nqcmap, false, QStringLiteral("")); + else if (index == Sections::SinceMemberFunctions + || index == Sections::SinceQmlMethods + || index == Sections::SinceQmlProperties) { + + QMap<QString, NodeMultiMap> parentmaps; + + const QList<Node *> &members = section.members(); + for (const auto &member : members) { + QString parent_full_name = (*member).parent()->fullName(); + + auto parent_entry = parentmaps.find(parent_full_name); + if (parent_entry == parentmaps.end()) + parent_entry = parentmaps.insert(parent_full_name, NodeMultiMap()); + parent_entry->insert(member->name(), member); + } + + for (auto map = parentmaps.begin(); map != parentmaps.end(); ++map) { + NodeVector nv = map->values().toVector(); + auto parent = nv.front()->parent(); + + out() << ((index == Sections::SinceMemberFunctions) ? "<p>Class " : "<p>QML Type "); + + out() << "<a href=\"" << linkForNode(parent, relative) << "\" translate=\"no\">"; + QStringList pieces = parent->fullName().split("::"); + out() << protectEnc(pieces.last()); + out() << "</a>" + << ":</p>\n"; + + generateSection(nv, relative, marker); + out() << "<br/>"; + } + } else if (index == Sections::SinceEnumValues) { + out() << "<div class=\"table\"><table class=\"alignedsummary\" translate=\"no\">\n"; + const auto map_it = m_qdb->newEnumValueMaps().constFind(atom->string()); + for (auto it = map_it->cbegin(); it != map_it->cend(); ++it) { + out() << "<tr><td class=\"memItemLeft\"> enum value </td><td class=\"memItemRight\">" + << "<b><a href=\"" << linkForNode(it.value(), nullptr) << "\">" + << it.key() << "</a></b></td></tr>\n"; + } + out() << "</table></div>\n"; + } else { + generateSection(section.members(), relative, marker); + } + } + ++index; + } + } break; + case Atom::BR: + out() << "<br />\n"; + break; + case Atom::HR: + out() << "<hr />\n"; + break; + case Atom::Image: + case Atom::InlineImage: { + QString text; + if (atom->next() && atom->next()->type() == Atom::ImageText) + text = atom->next()->string(); + if (atom->type() == Atom::Image) + out() << "<p class=\"centerAlign\">"; + + auto maybe_resolved_file{file_resolver.resolve(atom->string())}; + if (!maybe_resolved_file) { + // TODO: [uncentralized-admonition] + relative->location().warning( + QStringLiteral("Missing image: %1").arg(protectEnc(atom->string()))); + out() << "<font color=\"red\">[Missing image " << protectEnc(atom->string()) + << "]</font>"; + } else { + ResolvedFile file{*maybe_resolved_file}; + QString file_name{QFileInfo{file.get_path()}.fileName()}; + + // TODO: [operation-can-fail-making-the-output-incorrect] + // The operation of copying the file can fail, making the + // output refer to an image that does not exist. + // This should be fine as HTML will take care of managing + // the rendering of a missing image, but what html will + // render is in stark contrast with what we do when the + // image does not exist at all. + // It may be more correct to unify the behavior between + // the two either by considering images that cannot be + // copied as missing or letting the HTML renderer + // always taking care of the two cases. + // Do notice that effectively doing this might be + // unnecessary as extracting the output directory logic + // should ensure that a safe assumption for copy should be + // made at the API boundary. + + // TODO: [uncentralized-output-directory-structure] + Config::copyFile(relative->doc().location(), file.get_path(), file_name, outputDir() + QLatin1String("/images")); + + // TODO: [uncentralized-output-directory-structure] + out() << "<img src=\"" << "images/" + protectEnc(file_name) << '"'; + + // TODO: [same-result-branching] + // If text is empty protectEnc should return the empty + // string itself, such that the two branches would still + // result in the same output. + // Ensure that this is the case and then flatten the branch if so. + if (!text.isEmpty()) + out() << " alt=\"" << protectEnc(text) << '"'; + else + out() << " alt=\"\""; + + out() << " />"; + + // TODO: [uncentralized-output-directory-structure] + m_helpProjectWriter->addExtraFile("images/" + file_name); + setImageFileName(relative, "images/" + file_name); + } + + if (atom->type() == Atom::Image) + out() << "</p>"; + } break; + case Atom::ImageText: + break; + // Admonitions + case Atom::ImportantLeft: + case Atom::NoteLeft: + case Atom::WarningLeft: { + QString admonType = atom->typeString(); + // Remove 'Left' from atom type to get the admonition type + admonType.chop(4); + out() << "<div class=\"admonition " << admonType.toLower() << "\">\n" + << "<p>"; + out() << formattingLeftMap()[ATOM_FORMATTING_BOLD]; + out() << admonType << ": "; + out() << formattingRightMap()[ATOM_FORMATTING_BOLD]; + } break; + case Atom::ImportantRight: + case Atom::NoteRight: + case Atom::WarningRight: + out() << "</p>\n" + << "</div>\n"; + break; + case Atom::LegaleseLeft: + out() << "<div class=\"LegaleseLeft\">"; + break; + case Atom::LegaleseRight: + out() << "</div>"; + break; + case Atom::LineBreak: + out() << "<br/>"; + break; + case Atom::Link: + // Prevent nested links in table of contents + if (m_inContents) + break; + Q_FALLTHROUGH(); + case Atom::NavLink: { + const Node *node = nullptr; + QString link = getLink(atom, relative, &node); + if (link.isEmpty() && (node != relative) && !noLinkErrors()) { + Location location = atom->isLinkAtom() ? static_cast<const LinkAtom*>(atom)->location + : relative->doc().location(); + location.warning( + QStringLiteral("Can't link to '%1'").arg(atom->string())); + } + beginLink(link, node, relative); + skipAhead = 1; + } break; + case Atom::ExampleFileLink: { + QString link = linkForExampleFile(atom->string()); + beginLink(link); + skipAhead = 1; + } break; + case Atom::ExampleImageLink: { + QString link = atom->string(); + link = "images/used-in-examples/" + link; + beginLink(link); + skipAhead = 1; + } break; + case Atom::LinkNode: { + const Node *node = CodeMarker::nodeForString(atom->string()); + beginLink(linkForNode(node, relative), node, relative); + skipAhead = 1; + } break; + case Atom::ListLeft: + if (in_para) { + out() << "</p>\n"; + in_para = false; + } + if (atom->string() == ATOM_LIST_BULLET) { + out() << "<ul>\n"; + } else if (atom->string() == ATOM_LIST_TAG) { + out() << "<dl>\n"; + } else if (atom->string() == ATOM_LIST_VALUE) { + out() << R"(<div class="table"><table class="valuelist">)"; + m_threeColumnEnumValueTable = isThreeColumnEnumValueTable(atom); + if (m_threeColumnEnumValueTable) { + if (++m_numTableRows % 2 == 1) + out() << R"(<tr valign="top" class="odd">)"; + else + out() << R"(<tr valign="top" class="even">)"; + + out() << "<th class=\"tblConst\">Constant</th>"; + + // If not in \enum topic, skip the value column + if (relative->isEnumType()) + out() << "<th class=\"tblval\">Value</th>"; + + out() << "<th class=\"tbldscr\">Description</th></tr>\n"; + } else { + out() << "<tr><th class=\"tblConst\">Constant</th><th " + "class=\"tblVal\">Value</th></tr>\n"; + } + } else { + QString olType; + if (atom->string() == ATOM_LIST_UPPERALPHA) { + olType = "A"; + } else if (atom->string() == ATOM_LIST_LOWERALPHA) { + olType = "a"; + } else if (atom->string() == ATOM_LIST_UPPERROMAN) { + olType = "I"; + } else if (atom->string() == ATOM_LIST_LOWERROMAN) { + olType = "i"; + } else { // (atom->string() == ATOM_LIST_NUMERIC) + olType = "1"; + } + + if (atom->next() != nullptr && atom->next()->string().toInt() > 1) { + out() << QString(R"(<ol class="%1" type="%1" start="%2">)") + .arg(olType, atom->next()->string()); + } else + out() << QString(R"(<ol class="%1" type="%1">)").arg(olType); + } + break; + case Atom::ListItemNumber: + break; + case Atom::ListTagLeft: + if (atom->string() == ATOM_LIST_TAG) { + out() << "<dt>"; + } else { // (atom->string() == ATOM_LIST_VALUE) + std::pair<QString, int> pair = getAtomListValue(atom); + skipAhead = pair.second; + QString t = protectEnc(plainCode(marker->markedUpEnumValue(pair.first, relative))); + out() << "<tr><td class=\"topAlign\"><code translate=\"no\">" << t << "</code>"; + + if (relative->isEnumType()) { + out() << "</td><td class=\"topAlign tblval\">"; + const auto *enume = static_cast<const EnumNode *>(relative); + QString itemValue = enume->itemValue(atom->next()->string()); + if (itemValue.isEmpty()) + out() << '?'; + else + out() << "<code translate=\"no\">" << protectEnc(itemValue) << "</code>"; + } + } + break; + case Atom::SinceTagRight: + case Atom::ListTagRight: + if (atom->string() == ATOM_LIST_TAG) + out() << "</dt>\n"; + break; + case Atom::ListItemLeft: + if (atom->string() == ATOM_LIST_TAG) { + out() << "<dd>"; + } else if (atom->string() == ATOM_LIST_VALUE) { + if (m_threeColumnEnumValueTable) { + out() << "</td><td class=\"topAlign\">"; + if (matchAhead(atom, Atom::ListItemRight)) + out() << " "; + } + } else { + out() << "<li>"; + } + if (matchAhead(atom, Atom::ParaLeft)) + skipAhead = 1; + break; + case Atom::ListItemRight: + if (atom->string() == ATOM_LIST_TAG) { + out() << "</dd>\n"; + } else if (atom->string() == ATOM_LIST_VALUE) { + out() << "</td></tr>\n"; + } else { + out() << "</li>\n"; + } + break; + case Atom::ListRight: + if (atom->string() == ATOM_LIST_BULLET) { + out() << "</ul>\n"; + } else if (atom->string() == ATOM_LIST_TAG) { + out() << "</dl>\n"; + } else if (atom->string() == ATOM_LIST_VALUE) { + out() << "</table></div>\n"; + } else { + out() << "</ol>\n"; + } + break; + case Atom::Nop: + break; + case Atom::ParaLeft: + out() << "<p>"; + in_para = true; + break; + case Atom::ParaRight: + endLink(); + if (in_para) { + out() << "</p>\n"; + in_para = false; + } + // if (!matchAhead(atom, Atom::ListItemRight) && !matchAhead(atom, Atom::TableItemRight)) + // out() << "</p>\n"; + break; + case Atom::QuotationLeft: + out() << "<blockquote>"; + break; + case Atom::QuotationRight: + out() << "</blockquote>\n"; + break; + case Atom::RawString: + out() << atom->string(); + break; + case Atom::SectionLeft: + case Atom::SectionRight: + break; + case Atom::SectionHeadingLeft: { + int unit = atom->string().toInt() + hOffset(relative); + out() << "<h" + QString::number(unit) + QLatin1Char(' ') << "id=\"" + << Tree::refForAtom(atom) << "\">"; + m_inSectionHeading = true; + break; + } + case Atom::SectionHeadingRight: + out() << "</h" + QString::number(atom->string().toInt() + hOffset(relative)) + ">\n"; + m_inSectionHeading = false; + break; + case Atom::SidebarLeft: + Q_FALLTHROUGH(); + case Atom::SidebarRight: + break; + case Atom::String: + if (m_inLink && !m_inContents && !m_inSectionHeading) { + generateLink(atom); + } else { + out() << protectEnc(atom->string()); + } + break; + case Atom::TableLeft: { + std::pair<QString, QString> pair = getTableWidthAttr(atom); + QString attr = pair.second; + QString width = pair.first; + + if (in_para) { + out() << "</p>\n"; + in_para = false; + } + + out() << R"(<div class="table"><table class=")" << attr << '"'; + if (!width.isEmpty()) + out() << " width=\"" << width << '"'; + out() << ">\n "; + m_numTableRows = 0; + } break; + case Atom::TableRight: + out() << "</table></div>\n"; + break; + case Atom::TableHeaderLeft: + out() << "<thead><tr class=\"qt-style\">"; + m_inTableHeader = true; + break; + case Atom::TableHeaderRight: + out() << "</tr>"; + if (matchAhead(atom, Atom::TableHeaderLeft)) { + skipAhead = 1; + out() << "\n<tr class=\"qt-style\">"; + } else { + out() << "</thead>\n"; + m_inTableHeader = false; + } + break; + case Atom::TableRowLeft: + if (!atom->string().isEmpty()) + out() << "<tr " << atom->string() << '>'; + else if (++m_numTableRows % 2 == 1) + out() << R"(<tr valign="top" class="odd">)"; + else + out() << R"(<tr valign="top" class="even">)"; + break; + case Atom::TableRowRight: + out() << "</tr>\n"; + break; + case Atom::TableItemLeft: { + if (m_inTableHeader) + out() << "<th "; + else + out() << "<td "; + + for (int i = 0; i < atom->count(); ++i) { + if (i > 0) + out() << ' '; + const QString &p = atom->string(i); + if (p.contains('=')) { + out() << p; + } else { + QStringList spans = p.split(QLatin1Char(',')); + if (spans.size() == 2) { + if (spans.at(0) != "1") + out() << " colspan=\"" << spans.at(0) << '"'; + if (spans.at(1) != "1") + out() << " rowspan=\"" << spans.at(1) << '"'; + } + } + } + out() << '>'; + if (matchAhead(atom, Atom::ParaLeft)) + skipAhead = 1; + } break; + case Atom::TableItemRight: + if (m_inTableHeader) + out() << "</th>"; + else { + out() << "</td>"; + } + if (matchAhead(atom, Atom::ParaLeft)) + skipAhead = 1; + break; + case Atom::TableOfContents: + Q_FALLTHROUGH(); + case Atom::Keyword: + break; + case Atom::Target: + out() << "<span id=\"" << Utilities::asAsciiPrintable(atom->string()) << "\"></span>"; + break; + case Atom::UnhandledFormat: + out() << "<b class=\"redFont\"><Missing HTML></b>"; + break; + case Atom::UnknownCommand: + out() << R"(<b class="redFont"><code translate=\"no\">\)" << protectEnc(atom->string()) << "</code></b>"; + break; + case Atom::CodeQuoteArgument: + case Atom::CodeQuoteCommand: + case Atom::ComparesLeft: + case Atom::ComparesRight: + case Atom::SnippetCommand: + case Atom::SnippetIdentifier: + case Atom::SnippetLocation: + // no HTML output (ignore) + break; + default: + unknownAtom(atom); + } + return skipAhead; +} + +/*! + * Return a string representing a text that exposes information about + * the user-visible groups that the \a node is part of. A user-visible + * group is a group that generates an output page, that is, a \\group + * topic exists for the group and can be linked to. + * + * The returned string is composed of comma separated links to the + * groups, with their title as the user-facing text, surrounded by + * some introductory text. + * + * For example, if a node named N is part of the groups with title A + * and B, the line rendered form of the line will be "N is part of the + * A, B groups", where A and B are clickable links that target the + * respective page of each group. + * + * If a node has a single group, the comma is removed for readability + * pusposes and "groups" is expressed as a singular noun. + * For example, "N is part of the A group". + * + * The returned string is empty when the node is not linked to any + * group that has a valid link target. + * + * This string is used in the summary of c++ classes or qml types to + * link them to some of the overview documentation that is generated + * through the "\group" command. + * + * Note that this is currently, incorrectly, a member of + * HtmlGenerator as it requires access to some protected/private + * members for escaping and linking. + */ +QString HtmlGenerator::groupReferenceText(PageNode* node) { + auto link_for_group = [this](const CollectionNode *group) -> QString { + QString target{linkForNode(group, nullptr)}; + return (target.isEmpty()) ? protectEnc(group->name()) : "<a href=\"" + target + "\">" + protectEnc(group->fullTitle()) + "</a>"; + }; + + QString text{}; + + const QStringList &groups_names{node->groupNames()}; + if (groups_names.isEmpty()) + return text; + + std::vector<CollectionNode *> groups_nodes(groups_names.size(), nullptr); + std::transform(groups_names.cbegin(), groups_names.cend(), groups_nodes.begin(), + [this](const QString &group_name) -> CollectionNode* { + CollectionNode *group{m_qdb->groups()[group_name]}; + m_qdb->mergeCollections(group); + return (group && group->wasSeen()) ? group : nullptr; + }); + groups_nodes.erase(std::remove(groups_nodes.begin(), groups_nodes.end(), nullptr), groups_nodes.end()); + + if (!groups_nodes.empty()) { + text += node->name() + " is part of "; + + for (std::vector<CollectionNode *>::size_type index{0}; index < groups_nodes.size(); ++index) { + text += link_for_group(groups_nodes[index]) + Utilities::separator(index, groups_nodes.size()); + } + } + return text; +} + +/*! + Generate a reference page for the C++ class, namespace, or + header file documented in \a node using the code \a marker + provided. + */ +void HtmlGenerator::generateCppReferencePage(Aggregate *aggregate, CodeMarker *marker) +{ + QString title; + QString rawTitle; + QString fullTitle; + NamespaceNode *ns = nullptr; + SectionVector *summarySections = nullptr; + SectionVector *detailsSections = nullptr; + + Sections sections(aggregate); + QString word = aggregate->typeWord(true); + auto templateDecl = aggregate->templateDecl(); + if (aggregate->isNamespace()) { + rawTitle = aggregate->plainName(); + fullTitle = aggregate->plainFullName(); + title = rawTitle + " Namespace"; + ns = static_cast<NamespaceNode *>(aggregate); + summarySections = §ions.stdSummarySections(); + detailsSections = §ions.stdDetailsSections(); + } else if (aggregate->isClassNode()) { + rawTitle = aggregate->plainName(); + fullTitle = aggregate->plainFullName(); + title = rawTitle + QLatin1Char(' ') + word; + summarySections = §ions.stdCppClassSummarySections(); + detailsSections = §ions.stdCppClassDetailsSections(); + } else if (aggregate->isHeader()) { + title = fullTitle = rawTitle = aggregate->fullTitle(); + summarySections = §ions.stdSummarySections(); + detailsSections = §ions.stdDetailsSections(); + } + + Text subtitleText; + if (rawTitle != fullTitle || templateDecl) { + if (aggregate->isClassNode()) { + if (templateDecl) + subtitleText << (*templateDecl).to_qstring() + QLatin1Char(' '); + subtitleText << aggregate->typeWord(false) + QLatin1Char(' '); + const QStringList ancestors = fullTitle.split(QLatin1String("::")); + for (const auto &a : ancestors) { + if (a == rawTitle) { + subtitleText << a; + break; + } else { + subtitleText << Atom(Atom::AutoLink, a) << "::"; + } + } + } else { + subtitleText << fullTitle; + } + } + + generateHeader(title, aggregate, marker); + generateTableOfContents(aggregate, marker, summarySections); + generateTitle(title, subtitleText, SmallSubTitle, aggregate, marker); + if (ns && !ns->hasDoc() && ns->docNode()) { + NamespaceNode *NS = ns->docNode(); + Text brief; + brief << "The " << ns->name() << " namespace includes the following elements from module " + << ns->tree()->camelCaseModuleName() << ". The full namespace is " + << "documented in module " << NS->tree()->camelCaseModuleName() + << Atom(Atom::LinkNode, CodeMarker::stringForNode(NS)) + << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << Atom(Atom::String, " here.") + << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); + out() << "<p>"; + generateText(brief, ns, marker); + out() << "</p>\n"; + } else + generateBrief(aggregate, marker); + + const auto parentIsClass = aggregate->parent()->isClassNode(); + + if (!parentIsClass) + generateRequisites(aggregate, marker); + generateStatus(aggregate, marker); + if (parentIsClass) + generateSince(aggregate, marker); + + QString membersLink = generateAllMembersFile(Sections::allMembersSection(), marker); + if (!membersLink.isEmpty()) { + openUnorderedList(); + out() << "<li><a href=\"" << membersLink << "\">" + << "List of all members, including inherited members</a></li>\n"; + } + QString obsoleteLink = generateObsoleteMembersFile(sections, marker); + if (!obsoleteLink.isEmpty()) { + openUnorderedList(); + out() << "<li><a href=\"" << obsoleteLink << "\">" + << "Deprecated members</a></li>\n"; + } + + if (QString groups_text{groupReferenceText(aggregate)}; !groups_text.isEmpty()) { + openUnorderedList(); + + out() << "<li>" << groups_text << "</li>\n"; + } + + closeUnorderedList(); + generateComparisonCategory(aggregate, marker); + generateComparisonList(aggregate); + + generateThreadSafeness(aggregate, marker); + + bool needOtherSection = false; + + for (const auto §ion : std::as_const(*summarySections)) { + if (section.members().isEmpty() && section.reimplementedMembers().isEmpty()) { + if (!section.inheritedMembers().isEmpty()) + needOtherSection = true; + } else { + if (!section.members().isEmpty()) { + QString ref = registerRef(section.title().toLower()); + out() << "<h2 id=\"" << ref << "\">" << protectEnc(section.title()) << "</h2>\n"; + generateSection(section.members(), aggregate, marker); + } + if (!section.reimplementedMembers().isEmpty()) { + QString name = QString("Reimplemented ") + section.title(); + QString ref = registerRef(name.toLower()); + out() << "<h2 id=\"" << ref << "\">" << protectEnc(name) << "</h2>\n"; + generateSection(section.reimplementedMembers(), aggregate, marker); + } + + if (!section.inheritedMembers().isEmpty()) { + out() << "<ul>\n"; + generateSectionInheritedList(section, aggregate); + out() << "</ul>\n"; + } + } + } + + if (needOtherSection) { + out() << "<h3>Additional Inherited Members</h3>\n" + "<ul>\n"; + + for (const auto §ion : std::as_const(*summarySections)) { + if (section.members().isEmpty() && !section.inheritedMembers().isEmpty()) + generateSectionInheritedList(section, aggregate); + } + out() << "</ul>\n"; + } + + if (aggregate->doc().isEmpty()) { + QString command = "documentation"; + if (aggregate->isClassNode()) + command = R"('\class' comment)"; + if (!ns || ns->isDocumentedHere()) { + aggregate->location().warning( + QStringLiteral("No %1 for '%2'").arg(command, aggregate->plainSignature())); + } + } else { + generateExtractionMark(aggregate, DetailedDescriptionMark); + out() << "<div class=\"descr\">\n" + << "<h2 id=\"" << registerRef("details") << "\">" + << "Detailed Description" + << "</h2>\n"; + generateBody(aggregate, marker); + out() << "</div>\n"; + generateAlsoList(aggregate, marker); + generateExtractionMark(aggregate, EndMark); + } + + for (const auto §ion : std::as_const(*detailsSections)) { + bool headerGenerated = false; + if (section.isEmpty()) + continue; + + const QList<Node *> &members = section.members(); + for (const auto &member : members) { + if (member->access() == Access::Private) // ### check necessary? + continue; + if (!headerGenerated) { + if (!section.divClass().isEmpty()) + out() << "<div class=\"" << section.divClass() << "\">\n"; + out() << "<h2>" << protectEnc(section.title()) << "</h2>\n"; + headerGenerated = true; + } + if (!member->isClassNode()) + generateDetailedMember(member, aggregate, marker); + else { + out() << "<h3> class "; + generateFullName(member, aggregate); + out() << "</h3>"; + generateBrief(member, marker, aggregate); + } + + QStringList names; + names << member->name(); + if (member->isFunction()) { + const auto *func = reinterpret_cast<const FunctionNode *>(member); + if (func->isSomeCtor() || func->isDtor() || func->overloadNumber() != 0) + names.clear(); + } else if (member->isProperty()) { + const auto *prop = reinterpret_cast<const PropertyNode *>(member); + if (!prop->getters().isEmpty() && !names.contains(prop->getters().first()->name())) + names << prop->getters().first()->name(); + if (!prop->setters().isEmpty()) + names << prop->setters().first()->name(); + if (!prop->resetters().isEmpty()) + names << prop->resetters().first()->name(); + if (!prop->notifiers().isEmpty()) + names << prop->notifiers().first()->name(); + } else if (member->isEnumType()) { + const auto *enume = reinterpret_cast<const EnumNode *>(member); + if (enume->flagsType()) + names << enume->flagsType()->name(); + const auto &enumItemNameList = enume->doc().enumItemNames(); + const auto &omitEnumItemNameList = enume->doc().omitEnumItemNames(); + const auto items = QSet<QString>(enumItemNameList.cbegin(), enumItemNameList.cend()) + - QSet<QString>(omitEnumItemNameList.cbegin(), omitEnumItemNameList.cend()); + for (const QString &enumName : items) { + names << plainCode(marker->markedUpEnumValue(enumName, enume)); + } + } + } + if (headerGenerated && !section.divClass().isEmpty()) + out() << "</div>\n"; + } + generateFooter(aggregate); +} + +void HtmlGenerator::generateProxyPage(Aggregate *aggregate, CodeMarker *marker) +{ + Q_ASSERT(aggregate->isProxyNode()); + + QString title; + QString rawTitle; + QString fullTitle; + Text subtitleText; + SectionVector *summarySections = nullptr; + SectionVector *detailsSections = nullptr; + + Sections sections(aggregate); + rawTitle = aggregate->plainName(); + fullTitle = aggregate->plainFullName(); + title = rawTitle + " Proxy Page"; + summarySections = §ions.stdSummarySections(); + detailsSections = §ions.stdDetailsSections(); + generateHeader(title, aggregate, marker); + generateTitle(title, subtitleText, SmallSubTitle, aggregate, marker); + generateBrief(aggregate, marker); + for (auto it = summarySections->constBegin(); it != summarySections->constEnd(); ++it) { + if (!it->members().isEmpty()) { + QString ref = registerRef(it->title().toLower()); + out() << "<h2 id=\"" << ref << "\">" << protectEnc(it->title()) << "</h2>\n"; + generateSection(it->members(), aggregate, marker); + } + } + + if (!aggregate->doc().isEmpty()) { + generateExtractionMark(aggregate, DetailedDescriptionMark); + out() << "<div class=\"descr\">\n" + << "<h2 id=\"" << registerRef("details") << "\">" + << "Detailed Description" + << "</h2>\n"; + generateBody(aggregate, marker); + out() << "</div>\n"; + generateAlsoList(aggregate, marker); + generateExtractionMark(aggregate, EndMark); + } + + for (const auto §ion : std::as_const(*detailsSections)) { + if (section.isEmpty()) + continue; + + if (!section.divClass().isEmpty()) + out() << "<div class=\"" << section.divClass() << "\">\n"; + out() << "<h2>" << protectEnc(section.title()) << "</h2>\n"; + + const QList<Node *> &members = section.members(); + for (const auto &member : members) { + if (!member->isPrivate()) { // ### check necessary? + if (!member->isClassNode()) + generateDetailedMember(member, aggregate, marker); + else { + out() << "<h3> class "; + generateFullName(member, aggregate); + out() << "</h3>"; + generateBrief(member, marker, aggregate); + } + + QStringList names; + names << member->name(); + if (member->isFunction()) { + const auto *func = reinterpret_cast<const FunctionNode *>(member); + if (func->isSomeCtor() || func->isDtor() || func->overloadNumber() != 0) + names.clear(); + } else if (member->isEnumType()) { + const auto *enume = reinterpret_cast<const EnumNode *>(member); + if (enume->flagsType()) + names << enume->flagsType()->name(); + const auto &enumItemNameList = enume->doc().enumItemNames(); + const auto &omitEnumItemNameList = enume->doc().omitEnumItemNames(); + const auto items = + QSet<QString>(enumItemNameList.cbegin(), enumItemNameList.cend()) + - QSet<QString>(omitEnumItemNameList.cbegin(), + omitEnumItemNameList.cend()); + for (const QString &enumName : items) + names << plainCode(marker->markedUpEnumValue(enumName, enume)); + } + } + } + if (!section.divClass().isEmpty()) + out() << "</div>\n"; + } + generateFooter(aggregate); +} + +/*! + Generate the HTML page for a QML type. \qcn is the QML type. + \marker is the code markeup object. + */ +void HtmlGenerator::generateQmlTypePage(QmlTypeNode *qcn, CodeMarker *marker) +{ + Generator::setQmlTypeContext(qcn); + SubTitleSize subTitleSize = LargeSubTitle; + QString htmlTitle = qcn->fullTitle(); + if (qcn->isQmlBasicType()) + htmlTitle.append(" QML Value Type"); + else + htmlTitle.append(" QML Type"); + + + generateHeader(htmlTitle, qcn, marker); + Sections sections(qcn); + generateTableOfContents(qcn, marker, §ions.stdQmlTypeSummarySections()); + marker = CodeMarker::markerForLanguage(QLatin1String("QML")); + generateTitle(htmlTitle, Text() << qcn->subtitle(), subTitleSize, qcn, marker); + generateBrief(qcn, marker); + generateQmlRequisites(qcn, marker); + generateStatus(qcn, marker); + + QString allQmlMembersLink; + + // No 'All Members' file for QML value types + if (!qcn->isQmlBasicType()) + allQmlMembersLink = generateAllQmlMembersFile(sections, marker); + QString obsoleteLink = generateObsoleteQmlMembersFile(sections, marker); + if (!allQmlMembersLink.isEmpty() || !obsoleteLink.isEmpty()) { + openUnorderedList(); + + if (!allQmlMembersLink.isEmpty()) { + out() << "<li><a href=\"" << allQmlMembersLink << "\">" + << "List of all members, including inherited members</a></li>\n"; + } + if (!obsoleteLink.isEmpty()) { + out() << "<li><a href=\"" << obsoleteLink << "\">" + << "Deprecated members</a></li>\n"; + } + } + + if (QString groups_text{groupReferenceText(qcn)}; !groups_text.isEmpty()) { + openUnorderedList(); + + out() << "<li>" << groups_text << "</li>\n"; + } + + closeUnorderedList(); + + const QList<Section> &stdQmlTypeSummarySections = sections.stdQmlTypeSummarySections(); + for (const auto §ion : stdQmlTypeSummarySections) { + if (!section.isEmpty()) { + QString ref = registerRef(section.title().toLower()); + out() << "<h2 id=\"" << ref << "\">" << protectEnc(section.title()) << "</h2>\n"; + generateQmlSummary(section.members(), qcn, marker); + } + } + + generateExtractionMark(qcn, DetailedDescriptionMark); + out() << "<h2 id=\"" << registerRef("details") << "\">" + << "Detailed Description" + << "</h2>\n"; + generateBody(qcn, marker); + generateAlsoList(qcn, marker); + generateExtractionMark(qcn, EndMark); + + const QList<Section> &stdQmlTypeDetailsSections = sections.stdQmlTypeDetailsSections(); + for (const auto §ion : stdQmlTypeDetailsSections) { + if (!section.isEmpty()) { + out() << "<h2>" << protectEnc(section.title()) << "</h2>\n"; + const QList<Node *> &members = section.members(); + for (const auto member : members) { + generateDetailedQmlMember(member, qcn, marker); + out() << "<br/>\n"; + } + } + } + generateFooter(qcn); + Generator::setQmlTypeContext(nullptr); +} + +/*! + Generate the HTML page for an entity that doesn't map + to any underlying parsable C++ or QML element. + */ +void HtmlGenerator::generatePageNode(PageNode *pn, CodeMarker *marker) +{ + SubTitleSize subTitleSize = LargeSubTitle; + QString fullTitle = pn->fullTitle(); + + generateHeader(fullTitle, pn, marker); + /* + Generate the TOC for the new doc format. + Don't generate a TOC for the home page. + */ + if ((pn->name() != QLatin1String("index.html"))) + generateTableOfContents(pn, marker, nullptr); + + generateTitle(fullTitle, Text() << pn->subtitle(), subTitleSize, pn, marker); + if (pn->isExample()) { + generateBrief(pn, marker, nullptr, false); + } + + generateExtractionMark(pn, DetailedDescriptionMark); + out() << R"(<div class="descr" id=")" << registerRef("details") + << "\">\n"; + + generateBody(pn, marker); + out() << "</div>\n"; + generateAlsoList(pn, marker); + generateExtractionMark(pn, EndMark); + + generateFooter(pn); +} + +/*! + Generate the HTML page for a group, module, or QML module. + */ +void HtmlGenerator::generateCollectionNode(CollectionNode *cn, CodeMarker *marker) +{ + SubTitleSize subTitleSize = LargeSubTitle; + QString fullTitle = cn->fullTitle(); + QString ref; + + generateHeader(fullTitle, cn, marker); + generateTableOfContents(cn, marker, nullptr); + generateTitle(fullTitle, Text() << cn->subtitle(), subTitleSize, cn, marker); + + // Generate brief for C++ modules, status for all modules. + if (cn->genus() != Node::DOC && cn->genus() != Node::DontCare) { + if (cn->isModule()) + generateBrief(cn, marker); + generateStatus(cn, marker); + generateSince(cn, marker); + } + + if (cn->isModule()) { + if (!cn->noAutoList()) { + NodeMap nmm{cn->getMembers(Node::Namespace)}; + if (!nmm.isEmpty()) { + ref = registerRef("namespaces"); + out() << "<h2 id=\"" << ref << "\">Namespaces</h2>\n"; + generateAnnotatedList(cn, marker, nmm.values()); + } + nmm = cn->getMembers([](const Node *n){ return n->isClassNode(); }); + if (!nmm.isEmpty()) { + ref = registerRef("classes"); + out() << "<h2 id=\"" << ref << "\">Classes</h2>\n"; + generateAnnotatedList(cn, marker, nmm.values()); + } + } + } + + if (cn->isModule() && !cn->doc().briefText().isEmpty()) { + generateExtractionMark(cn, DetailedDescriptionMark); + ref = registerRef("details"); + out() << "<div class=\"descr\">\n"; + out() << "<h2 id=\"" << ref << "\">" + << "Detailed Description" + << "</h2>\n"; + } else { + generateExtractionMark(cn, DetailedDescriptionMark); + out() << R"(<div class="descr" id=")" << registerRef("details") + << "\">\n"; + } + + generateBody(cn, marker); + out() << "</div>\n"; + generateAlsoList(cn, marker); + generateExtractionMark(cn, EndMark); + + if (!cn->noAutoList()) { + if (cn->isGroup() || cn->isQmlModule()) + generateAnnotatedList(cn, marker, cn->members()); + } + generateFooter(cn); +} + +/*! + Generate the HTML page for a generic collection. This is usually + a collection of C++ elements that are related to an element in + a different module. + */ +void HtmlGenerator::generateGenericCollectionPage(CollectionNode *cn, CodeMarker *marker) +{ + SubTitleSize subTitleSize = LargeSubTitle; + QString fullTitle = cn->name(); + + generateHeader(fullTitle, cn, marker); + generateTitle(fullTitle, Text() << cn->subtitle(), subTitleSize, cn, marker); + + Text brief; + brief << "Each function or type documented here is related to a class or " + << "namespace that is documented in a different module. The reference " + << "page for that class or namespace will link to the function or type " + << "on this page."; + out() << "<p>"; + generateText(brief, cn, marker); + out() << "</p>\n"; + + const QList<Node *> members = cn->members(); + for (const auto &member : members) + generateDetailedMember(member, cn, marker); + + generateFooter(cn); +} + +/*! + Returns "html" for this subclass of Generator. + */ +QString HtmlGenerator::fileExtension() const +{ + return "html"; +} + +/*! + Output a navigation bar (breadcrumbs) for the html file. + For API reference pages, items for the navigation bar are (in order): + \table + \header \li Item \li Related configuration variable \li Notes + \row \li home \li navigation.homepage \li e.g. 'Qt 6.2' + \row \li landing \li navigation.landingpage \li Module landing page + \row \li types \li navigation.cppclassespage (C++)\br + navigation.qmltypespage (QML) \li Types only + \row \li module \li n/a (automatic) \li Module page if different + from previous item + \row \li page \li n/a \li Current page title + \endtable + + For other page types (page nodes) the navigation bar is constructed from home + page, landing page, and the chain of PageNode::navigationParent() items (if one exists). + This chain is constructed from the \\list structure on a page or pages defined in + \c navigation.toctitles configuration variable. + + Finally, if no other navigation data exists for a page but it is a member of a + single group (using \\ingroup), add that group page to the navigation bar. + */ +void HtmlGenerator::generateNavigationBar(const QString &title, const Node *node, + CodeMarker *marker, const QString &buildversion, + bool tableItems) +{ + if (m_noNavigationBar || node == nullptr) + return; + + Text navigationbar; + + // Set list item types based on the navigation bar type + // TODO: Do we still need table items? + Atom::AtomType itemLeft = tableItems ? Atom::TableItemLeft : Atom::ListItemLeft; + Atom::AtomType itemRight = tableItems ? Atom::TableItemRight : Atom::ListItemRight; + + // Helper to add an item to navigation bar based on a string link target + auto addNavItem = [&](const QString &link, const QString &title) { + navigationbar << Atom(itemLeft) << Atom(Atom::NavLink, link) + << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) + << Atom(Atom::String, title) + << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << Atom(itemRight); + }; + + // Helper to add an item to navigation bar based on a target node + auto addNavItemNode = [&](const Node *node, const QString &title) { + navigationbar << Atom(itemLeft) << Atom(Atom::LinkNode, CodeMarker::stringForNode(node)) + << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) + << Atom(Atom::String, title) + << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << Atom(itemRight); + }; + + // Resolve the associated module (collection) node and its 'state' description + const auto *moduleNode = m_qdb->getModuleNode(node); + QString moduleState; + if (moduleNode && !moduleNode->state().isEmpty()) + moduleState = QStringLiteral(" (%1)").arg(moduleNode->state()); + + if (m_hometitle == title) + return; + if (!m_homepage.isEmpty()) + addNavItem(m_homepage, m_hometitle); + if (!m_landingpage.isEmpty() && m_landingtitle != title) + addNavItem(m_landingpage, m_landingtitle); + + if (node->isClassNode()) { + if (!m_cppclassespage.isEmpty() && !m_cppclassestitle.isEmpty()) + addNavItem(m_cppclassespage, m_cppclassestitle); + if (!node->physicalModuleName().isEmpty()) { + // Add explicit link to the \module page if: + // - It's not the C++ classes page that's already added, OR + // - It has a \modulestate associated with it + if (moduleNode && (!moduleState.isEmpty() || moduleNode->title() != m_cppclassespage)) + addNavItemNode(moduleNode, moduleNode->name() + moduleState); + } + navigationbar << Atom(itemLeft) << Atom(Atom::String, node->name()) << Atom(itemRight); + } else if (node->isQmlType()) { + if (!m_qmltypespage.isEmpty() && !m_qmltypestitle.isEmpty()) + addNavItem(m_qmltypespage, m_qmltypestitle); + // Add explicit link to the \qmlmodule page if: + // - It's not the QML types page that's already added, OR + // - It has a \modulestate associated with it + if (moduleNode && (!moduleState.isEmpty() || moduleNode->title() != m_qmltypespage)) { + addNavItemNode(moduleNode, moduleNode->name() + moduleState); + } + navigationbar << Atom(itemLeft) << Atom(Atom::String, node->name()) << Atom(itemRight); + } else { + if (node->isPageNode()) { + auto currentNode{static_cast<const PageNode*>(node)}; + std::deque<const Node *> navNodes; + // Cutoff at 16 items in case there's a circular dependency + qsizetype navItems = 0; + while (currentNode->navigationParent() && ++navItems < 16) { + if (std::find(navNodes.cbegin(), navNodes.cend(), + currentNode->navigationParent()) == navNodes.cend()) + navNodes.push_front(currentNode->navigationParent()); + currentNode = currentNode->navigationParent(); + } + // If no nav. parent was found but the page is a \group member, add a link to the + // (first) group page. + if (navNodes.empty()) { + const QStringList groups = static_cast<const PageNode *>(node)->groupNames(); + for (const auto &groupName : groups) { + const auto *groupNode = m_qdb->findNodeByNameAndType(QStringList{groupName}, &Node::isGroup); + if (groupNode && !groupNode->title().isEmpty()) { + navNodes.push_front(groupNode); + break; + } + } + } + while (!navNodes.empty()) { + if (navNodes.front()->isPageNode()) + addNavItemNode(navNodes.front(), navNodes.front()->title()); + navNodes.pop_front(); + } + } + if (!navigationbar.isEmpty()) { + navigationbar << Atom(itemLeft) << Atom(Atom::String, title) << Atom(itemRight); + } + } + + generateText(navigationbar, node, marker); + + if (buildversion.isEmpty()) + return; + + navigationbar.clear(); + + if (tableItems) { + out() << "</tr></table><table class=\"buildversion\"><tr>\n" + << R"(<td id="buildversion" width="100%" align="right">)"; + } else { + out() << "<li id=\"buildversion\">"; + } + + // Link buildversion string to navigation.landingpage + if (!m_landingpage.isEmpty() && m_landingtitle != title) { + navigationbar << Atom(Atom::NavLink, m_landingpage) + << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) + << Atom(Atom::String, buildversion) + << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); + generateText(navigationbar, node, marker); + } else { + out() << buildversion; + } + if (tableItems) + out() << "</td>\n"; + else + out() << "</li>\n"; +} + +void HtmlGenerator::generateHeader(const QString &title, const Node *node, CodeMarker *marker) +{ + out() << "<!DOCTYPE html>\n"; + out() << QString("<html lang=\"%1\">\n").arg(naturalLanguage); + out() << "<head>\n"; + out() << " <meta charset=\"utf-8\">\n"; + if (node && !node->doc().location().isEmpty()) + out() << "<!-- " << node->doc().location().fileName() << " -->\n"; + + if (node && !node->doc().briefText().isEmpty()) { + out() << " <meta name=\"description\" content=\"" + << protectEnc(node->doc().briefText().toString()) + << "\">\n"; + } + + // determine the rest of the <title> element content: "title | titleSuffix version" + QString titleSuffix; + if (!m_landingtitle.isEmpty()) { + // for normal pages: "title | landingtitle version" + titleSuffix = m_landingtitle; + } else if (!m_hometitle.isEmpty()) { + // for pages that set the homepage title but not landing page title: + // "title | hometitle version" + if (title != m_hometitle) + titleSuffix = m_hometitle; + } else if (!m_project.isEmpty()) { + // for projects outside of Qt or Qt 5: "title | project version" + if (title != m_project) + titleSuffix = m_project; + } else + // default: "title | Qt version" + titleSuffix = QLatin1String("Qt "); + + if (title == titleSuffix) + titleSuffix.clear(); + + QString divider; + if (!titleSuffix.isEmpty() && !title.isEmpty()) + divider = QLatin1String(" | "); + + // Generating page title + out() << " <title>" << protectEnc(title) << divider << titleSuffix; + + // append a full version to the suffix if neither suffix nor title + // include (a prefix of) version information + QVersionNumber projectVersion = QVersionNumber::fromString(m_qdb->version()); + if (!projectVersion.isNull()) { + QVersionNumber titleVersion; + static const QRegularExpression re(QLatin1String(R"(\d+\.\d+)")); + const QString &versionedTitle = titleSuffix.isEmpty() ? title : titleSuffix; + auto match = re.match(versionedTitle); + if (match.hasMatch()) + titleVersion = QVersionNumber::fromString(match.captured()); + if (titleVersion.isNull() || !titleVersion.isPrefixOf(projectVersion)) + out() << QLatin1Char(' ') << projectVersion.toString(); + } + out() << "</title>\n"; + + // Include style sheet and script links. + out() << m_headerStyles; + out() << m_headerScripts; + if (m_endHeader.isEmpty()) + out() << "</head>\n<body>\n"; + else + out() << m_endHeader; + + out() << QString(m_postHeader).replace("\\" + COMMAND_VERSION, m_qdb->version()); + bool usingTable = m_postHeader.trimmed().endsWith(QLatin1String("<tr>")); + generateNavigationBar(title, node, marker, m_buildversion, usingTable); + out() << QString(m_postPostHeader).replace("\\" + COMMAND_VERSION, m_qdb->version()); + + m_navigationLinks.clear(); + refMap.clear(); + + if (node && !node->links().empty()) { + std::pair<QString, QString> linkPair; + std::pair<QString, QString> anchorPair; + const Node *linkNode; + bool useSeparator = false; + + if (node->links().contains(Node::PreviousLink)) { + linkPair = node->links()[Node::PreviousLink]; + linkNode = m_qdb->findNodeForTarget(linkPair.first, node); + if (linkNode == nullptr && !noLinkErrors()) + node->doc().location().warning( + QStringLiteral("Cannot link to '%1'").arg(linkPair.first)); + if (linkNode == nullptr || linkNode == node) + anchorPair = linkPair; + else + anchorPair = anchorForNode(linkNode); + + out() << R"( <link rel="prev" href=")" << anchorPair.first << "\" />\n"; + + m_navigationLinks += R"(<a class="prevPage" href=")" + anchorPair.first + "\">"; + if (linkPair.first == linkPair.second && !anchorPair.second.isEmpty()) + m_navigationLinks += protect(anchorPair.second); + else + m_navigationLinks += protect(linkPair.second); + m_navigationLinks += "</a>\n"; + useSeparator = !m_navigationSeparator.isEmpty(); + } + if (node->links().contains(Node::NextLink)) { + linkPair = node->links()[Node::NextLink]; + linkNode = m_qdb->findNodeForTarget(linkPair.first, node); + if (linkNode == nullptr && !noLinkErrors()) + node->doc().location().warning( + QStringLiteral("Cannot link to '%1'").arg(linkPair.first)); + if (linkNode == nullptr || linkNode == node) + anchorPair = linkPair; + else + anchorPair = anchorForNode(linkNode); + + out() << R"( <link rel="next" href=")" << anchorPair.first << "\" />\n"; + + if (useSeparator) + m_navigationLinks += m_navigationSeparator; + + m_navigationLinks += R"(<a class="nextPage" href=")" + anchorPair.first + "\">"; + if (linkPair.first == linkPair.second && !anchorPair.second.isEmpty()) + m_navigationLinks += protect(anchorPair.second); + else + m_navigationLinks += protect(linkPair.second); + m_navigationLinks += "</a>\n"; + } + if (node->links().contains(Node::StartLink)) { + linkPair = node->links()[Node::StartLink]; + linkNode = m_qdb->findNodeForTarget(linkPair.first, node); + if (linkNode == nullptr && !noLinkErrors()) + node->doc().location().warning( + QStringLiteral("Cannot link to '%1'").arg(linkPair.first)); + if (linkNode == nullptr || linkNode == node) + anchorPair = linkPair; + else + anchorPair = anchorForNode(linkNode); + out() << R"( <link rel="start" href=")" << anchorPair.first << "\" />\n"; + } + } + + if (node && !node->links().empty()) + out() << "<p class=\"naviNextPrevious headerNavi\">\n" << m_navigationLinks << "</p>\n"; +} + +void HtmlGenerator::generateTitle(const QString &title, const Text &subtitle, + SubTitleSize subTitleSize, const Node *relative, + CodeMarker *marker) +{ + out() << QString(m_prologue).replace("\\" + COMMAND_VERSION, m_qdb->version()); + QString attribute; + if (relative->genus() & Node::API) + attribute = R"( translate="no")"; + + if (!title.isEmpty()) + out() << "<h1 class=\"title\"" << attribute << ">" << protectEnc(title) << "</h1>\n"; + if (!subtitle.isEmpty()) { + out() << "<span"; + if (subTitleSize == SmallSubTitle) + out() << " class=\"small-subtitle\"" << attribute << ">"; + else + out() << " class=\"subtitle\"" << attribute << ">"; + generateText(subtitle, relative, marker); + out() << "</span>\n"; + } +} + +void HtmlGenerator::generateFooter(const Node *node) +{ + if (node && !node->links().empty()) + out() << "<p class=\"naviNextPrevious footerNavi\">\n" << m_navigationLinks << "</p>\n"; + + out() << QString(m_footer).replace("\\" + COMMAND_VERSION, m_qdb->version()) + << QString(m_address).replace("\\" + COMMAND_VERSION, m_qdb->version()); + + out() << "</body>\n"; + out() << "</html>\n"; +} + +/*! +Lists the required imports and includes in a table. +The number of rows is known. +*/ +void HtmlGenerator::generateRequisites(Aggregate *aggregate, CodeMarker *marker) +{ + QMap<QString, Text> requisites; + Text text; + + const QString headerText = "Header"; + const QString sinceText = "Since"; + const QString inheritedBytext = "Inherited By"; + const QString inheritsText = "Inherits"; + const QString nativeTypeText = "In QML"; + const QString qtVariableText = "qmake"; + const QString cmakeText = "CMake"; + const QString statusText = "Status"; + + // The order of the requisites matter + const QStringList requisiteorder { headerText, cmakeText, qtVariableText, sinceText, + nativeTypeText, inheritsText, inheritedBytext, statusText }; + + addIncludeFileToMap(aggregate, marker, requisites, text, headerText); + addSinceToMap(aggregate, requisites, &text, sinceText); + + if (aggregate->isClassNode() || aggregate->isNamespace()) { + addCMakeInfoToMap(aggregate, requisites, &text, cmakeText); + addQtVariableToMap(aggregate, requisites, &text, qtVariableText); + } + + if (aggregate->isClassNode()) { + auto *classe = dynamic_cast<ClassNode *>(aggregate); + if (classe->isQmlNativeType() && !classe->isInternal()) + addQmlNativeTypesToMap(requisites, &text, nativeTypeText, classe); + + addInheritsToMap(requisites, &text, inheritsText, classe); + addInheritedByToMap(requisites, &text, inheritedBytext, classe); + } + + // Add the state description (if any) to the map + addStatusToMap(aggregate, requisites, text, statusText); + + if (!requisites.isEmpty()) { + // generate the table + generateTheTable(requisiteorder, requisites, headerText, aggregate, marker); + } +} + +/*! + * \internal + */ +void HtmlGenerator::generateTheTable(const QStringList &requisiteOrder, + const QMap<QString, Text> &requisites, + const QString &headerText, const Aggregate *aggregate, + CodeMarker *marker) +{ + out() << "<div class=\"table\"><table class=\"alignedsummary\" translate=\"no\">\n"; + + for (auto it = requisiteOrder.constBegin(); it != requisiteOrder.constEnd(); ++it) { + + if (requisites.contains(*it)) { + out() << "<tr>" + << "<td class=\"memItemLeft rightAlign topAlign\"> " << *it + << ":" + "</td><td class=\"memItemRight bottomAlign\"> "; + + if (*it == headerText) + out() << requisites.value(*it).toString(); + else + generateText(requisites.value(*it), aggregate, marker); + out() << "</td></tr>\n"; + } + } + out() << "</table></div>\n"; +} + +/*! + * \internal + * Adds inherited by information to the map. + */ +void HtmlGenerator::addInheritedByToMap(QMap<QString, Text> &requisites, Text *text, + const QString &inheritedBytext, ClassNode *classe) +{ + if (!classe->derivedClasses().isEmpty()) { + text->clear(); + *text << Atom::ParaLeft; + int count = appendSortedNames(*text, classe, classe->derivedClasses()); + *text << Atom::ParaRight; + if (count > 0) + requisites.insert(inheritedBytext, *text); + } +} + +/*! + * \internal + * Adds base classes to the map. + */ +void HtmlGenerator::addInheritsToMap(QMap<QString, Text> &requisites, Text *text, + const QString &inheritsText, ClassNode *classe) +{ + if (!classe->baseClasses().isEmpty()) { + int index = 0; + text->clear(); + const auto baseClasses = classe->baseClasses(); + for (const auto &cls : baseClasses) { + if (cls.m_node) { + appendFullName(*text, cls.m_node, classe); + + if (cls.m_access == Access::Protected) { + *text << " (protected)"; + } else if (cls.m_access == Access::Private) { + *text << " (private)"; + } + *text << Utilities::comma(index++, classe->baseClasses().size()); + } + } + *text << Atom::ParaRight; + if (index > 0) + requisites.insert(inheritsText, *text); + } +} + +/*! + \internal + Add the QML/C++ native type information to the map. + */ + void HtmlGenerator::addQmlNativeTypesToMap(QMap<QString, Text> &requisites, Text *text, + const QString &nativeTypeText, ClassNode *classe) const +{ + if (!text) + return; + + text->clear(); + + QList<QmlTypeNode *> nativeTypes { classe->qmlNativeTypes().cbegin(), classe->qmlNativeTypes().cend()}; + std::sort(nativeTypes.begin(), nativeTypes.end(), Node::nodeNameLessThan); + qsizetype index { 0 }; + + for (const auto &item : std::as_const(nativeTypes)) { + *text << Atom(Atom::LinkNode, CodeMarker::stringForNode(item)) + << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) + << Atom(Atom::String, item->name()) + << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); + *text << Utilities::comma(index++, nativeTypes.size()); + } + requisites.insert(nativeTypeText, *text); +} + +/*! + * \internal + * Adds the CMake package and link library information to the map. + */ +void HtmlGenerator::addCMakeInfoToMap(const Aggregate *aggregate, QMap<QString, Text> &requisites, + Text *text, const QString &CMakeInfo) const +{ + if (!aggregate->physicalModuleName().isEmpty() && text != nullptr) { + const CollectionNode *cn = + m_qdb->getCollectionNode(aggregate->physicalModuleName(), Node::Module); + if (!cn || cn->qtCMakeComponent().isEmpty()) + return; + + text->clear(); + const QString qtComponent = "Qt" + QString::number(QT_VERSION_MAJOR); + const QString findPackageText = "find_package(" + qtComponent + " REQUIRED COMPONENTS " + + cn->qtCMakeComponent() + ")"; + const QString targetText = cn->qtCMakeTargetItem().isEmpty() ? cn->qtCMakeComponent() : cn->qtCMakeTargetItem(); + const QString targetLinkLibrariesText = "target_link_libraries(mytarget PRIVATE " + + qtComponent + "::" + targetText + ")"; + const Atom lineBreak = Atom(Atom::RawString, " <br/>\n"); + *text << findPackageText << lineBreak << targetLinkLibrariesText; + requisites.insert(CMakeInfo, *text); + } +} + +/*! + * \internal + * Adds the Qt variable (from the \\qtvariable command) to the map. + */ +void HtmlGenerator::addQtVariableToMap(const Aggregate *aggregate, QMap<QString, Text> &requisites, + Text *text, const QString &qtVariableText) const +{ + if (!aggregate->physicalModuleName().isEmpty()) { + const CollectionNode *cn = + m_qdb->getCollectionNode(aggregate->physicalModuleName(), Node::Module); + + if (cn && !cn->qtVariable().isEmpty()) { + text->clear(); + *text << "QT += " + cn->qtVariable(); + requisites.insert(qtVariableText, *text); + } + } +} + +/*! + * \internal + * Adds the since information (from the \\since command) to the map. + * + */ +void HtmlGenerator::addSinceToMap(const Aggregate *aggregate, QMap<QString, Text> &requisites, + Text *text, const QString &sinceText) const +{ + if (!aggregate->since().isEmpty() && text != nullptr) { + text->clear(); + *text << formatSince(aggregate) << Atom::ParaRight; + requisites.insert(sinceText, *text); + } +} + +/*! + * \internal + * Adds the status description for \a aggregate, together with a <span> element, to the \a + * requisites map. + * + * The span element can be used for adding CSS styling/icon associated with a specific status. + * The span class name is constructed by converting the description (sans \\deprecated + * version info) to lowercase and replacing all non-alphanum characters with hyphens. In + * addition, the span has a class \c status. For example, + * 'Tech Preview' -> class="status tech-preview" +*/ +void HtmlGenerator::addStatusToMap(const Aggregate *aggregate, QMap<QString, Text> &requisites, + Text &text, const QString &statusText) const +{ + auto status{formatStatus(aggregate, m_qdb)}; + if (!status) + return; + + QString spanClass; + if (aggregate->status() == Node::Deprecated) + spanClass = u"deprecated"_s; // Disregard any version info + else + spanClass = Utilities::asAsciiPrintable(status.value()); + + text.clear(); + text << Atom(Atom::String, status.value()) + << Atom(Atom::FormattingLeft, ATOM_FORMATTING_SPAN + + "class=\"status %1\""_L1.arg(spanClass)) + << Atom(Atom::FormattingRight, ATOM_FORMATTING_SPAN); + requisites.insert(statusText, text); +} + +/*! + * \internal + * Adds the includes (from the \\includefile command) to the map. + */ +void HtmlGenerator::addIncludeFileToMap(const Aggregate *aggregate, CodeMarker *marker, + QMap<QString, Text> &requisites, Text& text, + const QString &headerText) +{ + if (aggregate->includeFile()) { + text.clear(); + text << highlightedCode( + indent(m_codeIndent, marker->markedUpInclude(*aggregate->includeFile())), + aggregate + ); + + requisites.insert(headerText, text); + } +} + +/*! +Lists the required imports and includes in a table. +The number of rows is known. +*/ +void HtmlGenerator::generateQmlRequisites(QmlTypeNode *qcn, CodeMarker *marker) +{ + if (qcn == nullptr) + return; + QMap<QString, Text> requisites; + Text text; + + const QString importText = "Import Statement:"; + const QString sinceText = "Since:"; + const QString inheritedBytext = "Inherited By:"; + const QString inheritsText = "Inherits:"; + const QString nativeTypeText = "In C++:"; + const QString statusText = "Status:"; + + // add the module name and version to the map + QString logicalModuleVersion; + const CollectionNode *collection = qcn->logicalModule(); + + // skip import statement of \internal collections + if (!qcn->logicalModuleName().isEmpty() && (!collection || !collection->isInternal() || m_showInternal)) { + QStringList parts = QStringList() << "import" << qcn->logicalModuleName() << qcn->logicalModuleVersion(); + text.clear(); + text << parts.join(' ').trimmed(); + requisites.insert(importText, text); + } else if (!qcn->isQmlBasicType() && qcn->logicalModuleName().isEmpty()) { + qcn->doc().location().warning(QStringLiteral("Could not resolve QML import statement for type '%1'").arg(qcn->name()), + QStringLiteral("Maybe you forgot to use the '\\%1' command?").arg(COMMAND_INQMLMODULE)); + } + + // add the since and project into the map + if (!qcn->since().isEmpty()) { + text.clear(); + text << formatSince(qcn) << Atom::ParaRight; + requisites.insert(sinceText, text); + } + + // add the native type to the map + ClassNode *cn = qcn->classNode(); + if (cn && cn->isQmlNativeType() && !cn->isInternal()) { + text.clear(); + text << Atom(Atom::LinkNode, CodeMarker::stringForNode(cn)); + text << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK); + text << Atom(Atom::String, cn->name()); + text << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); + requisites.insert(nativeTypeText, text); + } + + // add the inherits to the map + QmlTypeNode *base = qcn->qmlBaseNode(); + while (base && base->isInternal()) { + base = base->qmlBaseNode(); + } + if (base) { + text.clear(); + text << Atom::ParaLeft << Atom(Atom::LinkNode, CodeMarker::stringForNode(base)) + << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << Atom(Atom::String, base->name()) + << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << Atom::ParaRight; + requisites.insert(inheritsText, text); + } + + // add the inherited-by to the map + NodeList subs; + QmlTypeNode::subclasses(qcn, subs); + if (!subs.isEmpty()) { + text.clear(); + text << Atom::ParaLeft; + int count = appendSortedQmlNames(text, qcn, subs); + text << Atom::ParaRight; + if (count > 0) + requisites.insert(inheritedBytext, text); + } + + // Add the state description (if any) to the map + addStatusToMap(qcn, requisites, text, statusText); + + // The order of the requisites matter + const QStringList requisiteorder { importText, sinceText, nativeTypeText, inheritsText, + inheritedBytext, statusText }; + + if (!requisites.isEmpty()) { + // generate the table + out() << "<div class=\"table\"><table class=\"alignedsummary\" translate=\"no\">\n"; + for (const auto &requisite : requisiteorder) { + + if (requisites.contains(requisite)) { + out() << "<tr>" + << "<td class=\"memItemLeft rightAlign topAlign\"> " << requisite + << "</td><td class=\"memItemRight bottomAlign\"> "; + + if (requisite == importText) + out() << requisites.value(requisite).toString(); + else + generateText(requisites.value(requisite), qcn, marker); + out() << "</td></tr>"; + } + } + out() << "</table></div>"; + } +} + +void HtmlGenerator::generateBrief(const Node *node, CodeMarker *marker, const Node *relative, + bool addLink) +{ + Text brief = node->doc().briefText(); + + if (!brief.isEmpty()) { + if (!brief.lastAtom()->string().endsWith('.')) { + brief << Atom(Atom::String, "."); + node->doc().location().warning( + QStringLiteral("'\\brief' statement does not end with a full stop.")); + } + generateExtractionMark(node, BriefMark); + out() << "<p>"; + generateText(brief, node, marker); + + if (addLink) { + if (!relative || node == relative) + out() << " <a href=\"#"; + else + out() << " <a href=\"" << linkForNode(node, relative) << '#'; + out() << registerRef("details") << "\">More...</a>"; + } + + out() << "</p>\n"; + generateExtractionMark(node, EndMark); + } +} + +/*! + Revised for the new doc format. + Generates a table of contents beginning at \a node. + */ +void HtmlGenerator::generateTableOfContents(const Node *node, CodeMarker *marker, + QList<Section> *sections) +{ + QList<Atom *> toc; + if (node->doc().hasTableOfContents()) + toc = node->doc().tableOfContents(); + if (tocDepth == 0 || (toc.isEmpty() && !sections && !node->isModule())) { + generateSidebar(); + return; + } + + int sectionNumber = 1; + int detailsBase = 0; + + // disable nested links in table of contents + m_inContents = true; + + out() << "<div class=\"sidebar\">\n"; + out() << "<div class=\"toc\">\n"; + out() << "<h3 id=\"toc\">Contents</h3>\n"; + + if (node->isModule()) { + openUnorderedList(); + if (!static_cast<const CollectionNode *>(node)->noAutoList()) { + if (node->hasNamespaces()) { + out() << "<li class=\"level" << sectionNumber << "\"><a href=\"#" + << registerRef("namespaces") << "\">Namespaces</a></li>\n"; + } + if (node->hasClasses()) { + out() << "<li class=\"level" << sectionNumber << "\"><a href=\"#" + << registerRef("classes") << "\">Classes</a></li>\n"; + } + } + out() << "<li class=\"level" << sectionNumber << "\"><a href=\"#" << registerRef("details") + << "\">Detailed Description</a></li>\n"; + for (const auto &entry : std::as_const(toc)) { + if (entry->string().toInt() == 1) { + detailsBase = 1; + break; + } + } + } else if (sections && (node->isClassNode() || node->isNamespace() || node->isQmlType())) { + for (const auto §ion : std::as_const(*sections)) { + if (!section.members().isEmpty()) { + openUnorderedList(); + out() << "<li class=\"level" << sectionNumber << "\"><a href=\"#" + << registerRef(section.plural()) << "\">" << section.title() << "</a></li>\n"; + } + if (!section.reimplementedMembers().isEmpty()) { + openUnorderedList(); + QString ref = QString("Reimplemented ") + section.plural(); + out() << "<li class=\"level" << sectionNumber << "\"><a href=\"#" + << registerRef(ref.toLower()) << "\">" + << QString("Reimplemented ") + section.title() << "</a></li>\n"; + } + } + if (!node->isNamespace() || node->hasDoc()) { + openUnorderedList(); + out() << "<li class=\"level" << sectionNumber << "\"><a href=\"#" + << registerRef("details") << "\">Detailed Description</a></li>\n"; + } + for (const auto &entry : toc) { + if (entry->string().toInt() == 1) { + detailsBase = 1; + break; + } + } + } + + for (const auto &atom : toc) { + sectionNumber = atom->string().toInt() + detailsBase; + // restrict the ToC depth to the one set by the HTML.tocdepth variable or + // print all levels if tocDepth is not set. + if (sectionNumber <= tocDepth || tocDepth < 0) { + openUnorderedList(); + int numAtoms; + Text headingText = Text::sectionHeading(atom); + out() << "<li class=\"level" << sectionNumber << "\">"; + out() << "<a href=\"" << '#' << Tree::refForAtom(atom) << "\">"; + generateAtomList(headingText.firstAtom(), node, marker, true, numAtoms); + out() << "</a></li>\n"; + } + } + closeUnorderedList(); + out() << "</div>\n"; + out() << R"(<div class="sidebar-content" id="sidebar-content"></div>)"; + out() << "</div>\n"; + m_inContents = false; + m_inLink = false; +} + +/*! + Outputs a placeholder div where the style can add customized sidebar content. + */ +void HtmlGenerator::generateSidebar() +{ + out() << "<div class=\"sidebar\">"; + out() << R"(<div class="sidebar-content" id="sidebar-content"></div>)"; + out() << "</div>\n"; +} + +QString HtmlGenerator::generateAllMembersFile(const Section §ion, CodeMarker *marker) +{ + if (section.isEmpty()) + return QString(); + + const Aggregate *aggregate = section.aggregate(); + QString fileName = fileBase(aggregate) + "-members." + fileExtension(); + beginSubPage(aggregate, fileName); + QString title = "List of All Members for " + aggregate->name(); + generateHeader(title, aggregate, marker); + generateSidebar(); + generateTitle(title, Text(), SmallSubTitle, aggregate, marker); + out() << "<p>This is the complete list of members for "; + generateFullName(aggregate, nullptr); + out() << ", including inherited members.</p>\n"; + + generateSectionList(section, aggregate, marker); + + generateFooter(); + endSubPage(); + return fileName; +} + +/*! + This function creates an html page on which are listed all + the members of the QML class used to generte the \a sections, + including the inherited members. The \a marker is used for + formatting stuff. + */ +QString HtmlGenerator::generateAllQmlMembersFile(const Sections §ions, CodeMarker *marker) +{ + + if (sections.allMembersSection().isEmpty()) + return QString(); + + const Aggregate *aggregate = sections.aggregate(); + QString fileName = fileBase(aggregate) + "-members." + fileExtension(); + beginSubPage(aggregate, fileName); + QString title = "List of All Members for " + aggregate->name(); + generateHeader(title, aggregate, marker); + generateSidebar(); + generateTitle(title, Text(), SmallSubTitle, aggregate, marker); + out() << "<p>This is the complete list of members for "; + generateFullName(aggregate, nullptr); + out() << ", including inherited members.</p>\n"; + + ClassNodesList &cknl = sections.allMembersSection().classNodesList(); + for (int i = 0; i < cknl.size(); i++) { + ClassNodes ckn = cknl[i]; + const QmlTypeNode *qcn = ckn.first; + NodeVector &nodes = ckn.second; + if (nodes.isEmpty()) + continue; + if (i != 0) { + out() << "<p>The following members are inherited from "; + generateFullName(qcn, nullptr); + out() << ".</p>\n"; + } + openUnorderedList(); + for (int j = 0; j < nodes.size(); j++) { + Node *node = nodes[j]; + if (node->access() == Access::Private || node->isInternal()) + continue; + if (node->isSharingComment() && node->sharedCommentNode()->isPropertyGroup()) + continue; + + std::function<void(Node *)> generate = [&](Node *n) { + out() << "<li class=\"fn\" translate=\"no\">"; + generateQmlItem(n, aggregate, marker, true); + if (n->isDefault()) + out() << " [default]"; + else if (n->isAttached()) + out() << " [attached]"; + // Indent property group members + if (n->isPropertyGroup()) { + out() << "<ul>\n"; + const QList<Node *> &collective = + static_cast<SharedCommentNode *>(n)->collective(); + std::for_each(collective.begin(), collective.end(), generate); + out() << "</ul>\n"; + } + out() << "</li>\n"; + }; + generate(node); + } + closeUnorderedList(); + } + + + generateFooter(); + endSubPage(); + return fileName; +} + +QString HtmlGenerator::generateObsoleteMembersFile(const Sections §ions, CodeMarker *marker) +{ + SectionPtrVector summary_spv; + SectionPtrVector details_spv; + if (!sections.hasObsoleteMembers(&summary_spv, &details_spv)) + return QString(); + + Aggregate *aggregate = sections.aggregate(); + QString title = "Obsolete Members for " + aggregate->name(); + QString fileName = fileBase(aggregate) + "-obsolete." + fileExtension(); + + beginSubPage(aggregate, fileName); + generateHeader(title, aggregate, marker); + generateSidebar(); + generateTitle(title, Text(), SmallSubTitle, aggregate, marker); + + out() << "<p><b>The following members of class " + << "<a href=\"" << linkForNode(aggregate, nullptr) << "\" translate=\"no\">" + << protectEnc(aggregate->name()) << "</a>" + << " are deprecated.</b> " + << "They are provided to keep old source code working. " + << "We strongly advise against using them in new code.</p>\n"; + + for (const auto §ion : summary_spv) { + out() << "<h2>" << protectEnc(section->title()) << "</h2>\n"; + generateSectionList(*section, aggregate, marker, true); + } + + for (const auto §ion : details_spv) { + out() << "<h2>" << protectEnc(section->title()) << "</h2>\n"; + + const NodeVector &members = section->obsoleteMembers(); + for (const auto &member : members) { + if (member->access() != Access::Private) + generateDetailedMember(member, aggregate, marker); + } + } + + generateFooter(); + endSubPage(); + return fileName; +} + +/*! + Generates a separate file where deprecated members of the QML + type \a qcn are listed. The \a marker is used to generate + the section lists, which are then traversed and output here. + */ +QString HtmlGenerator::generateObsoleteQmlMembersFile(const Sections §ions, CodeMarker *marker) +{ + SectionPtrVector summary_spv; + SectionPtrVector details_spv; + if (!sections.hasObsoleteMembers(&summary_spv, &details_spv)) + return QString(); + + Aggregate *aggregate = sections.aggregate(); + QString title = "Obsolete Members for " + aggregate->name(); + QString fileName = fileBase(aggregate) + "-obsolete." + fileExtension(); + + beginSubPage(aggregate, fileName); + generateHeader(title, aggregate, marker); + generateSidebar(); + generateTitle(title, Text(), SmallSubTitle, aggregate, marker); + + out() << "<p><b>The following members of QML type " + << "<a href=\"" << linkForNode(aggregate, nullptr) << "\">" + << protectEnc(aggregate->name()) << "</a>" + << " are deprecated.</b> " + << "They are provided to keep old source code working. " + << "We strongly advise against using them in new code.</p>\n"; + + for (const auto §ion : summary_spv) { + QString ref = registerRef(section->title().toLower()); + out() << "<h2 id=\"" << ref << "\">" << protectEnc(section->title()) << "</h2>\n"; + generateQmlSummary(section->obsoleteMembers(), aggregate, marker); + } + + for (const auto §ion : details_spv) { + out() << "<h2>" << protectEnc(section->title()) << "</h2>\n"; + const NodeVector &members = section->obsoleteMembers(); + for (const auto &member : members) { + generateDetailedQmlMember(member, aggregate, marker); + out() << "<br/>\n"; + } + } + + generateFooter(); + endSubPage(); + return fileName; +} + +void HtmlGenerator::generateClassHierarchy(const Node *relative, NodeMultiMap &classMap) +{ + if (classMap.isEmpty()) + return; + + NodeMap topLevel; + for (const auto &it : classMap) { + auto *classe = static_cast<ClassNode *>(it); + if (classe->baseClasses().isEmpty()) + topLevel.insert(classe->name(), classe); + } + + QStack<NodeMap> stack; + stack.push(topLevel); + + out() << "<ul>\n"; + while (!stack.isEmpty()) { + if (stack.top().isEmpty()) { + stack.pop(); + out() << "</ul>\n"; + } else { + ClassNode *child = static_cast<ClassNode *>(*stack.top().begin()); + out() << "<li>"; + generateFullName(child, relative); + out() << "</li>\n"; + stack.top().erase(stack.top().begin()); + + NodeMap newTop; + const auto derivedClasses = child->derivedClasses(); + for (const RelatedClass &d : derivedClasses) { + if (d.m_node && d.m_node->isInAPI()) + newTop.insert(d.m_node->name(), d.m_node); + } + if (!newTop.isEmpty()) { + stack.push(newTop); + out() << "<ul>\n"; + } + } + } +} + +/*! + Outputs an annotated list of the nodes in \a unsortedNodes. + A two-column table is output. + */ +void HtmlGenerator::generateAnnotatedList(const Node *relative, CodeMarker *marker, + const NodeList &unsortedNodes, Qt::SortOrder sortOrder) +{ + if (unsortedNodes.isEmpty() || relative == nullptr) + return; + + NodeMultiMap nmm; + bool allInternal = true; + for (auto *node : unsortedNodes) { + if (!node->isInternal() && !node->isDeprecated()) { + allInternal = false; + nmm.insert(node->fullName(relative), node); + } + } + if (allInternal) + return; + out() << "<div class=\"table\"><table class=\"annotated\">\n"; + int row = 0; + NodeList nodes = nmm.values(); + + if (sortOrder == Qt::DescendingOrder) + std::sort(nodes.rbegin(), nodes.rend(), Node::nodeSortKeyOrNameLessThan); + else + std::sort(nodes.begin(), nodes.end(), Node::nodeSortKeyOrNameLessThan); + + for (const auto *node : std::as_const(nodes)) { + if (++row % 2 == 1) + out() << "<tr class=\"odd topAlign\">"; + else + out() << "<tr class=\"even topAlign\">"; + out() << "<td class=\"tblName\" translate=\"no\"><p>"; + generateFullName(node, relative); + out() << "</p></td>"; + + if (!node->isTextPageNode()) { + Text brief = node->doc().trimmedBriefText(node->name()); + if (!brief.isEmpty()) { + out() << "<td class=\"tblDescr\"><p>"; + generateText(brief, node, marker); + out() << "</p></td>"; + } else if (!node->reconstitutedBrief().isEmpty()) { + out() << "<td class=\"tblDescr\"><p>"; + out() << node->reconstitutedBrief(); + out() << "</p></td>"; + } + } else { + out() << "<td class=\"tblDescr\"><p>"; + if (!node->reconstitutedBrief().isEmpty()) { + out() << node->reconstitutedBrief(); + } else + out() << protectEnc(node->doc().briefText().toString()); + out() << "</p></td>"; + } + out() << "</tr>\n"; + } + out() << "</table></div>\n"; +} + +/*! + Outputs a series of annotated lists from the nodes in \a nmm, + divided into sections based by the key names in the multimap. + */ +void HtmlGenerator::generateAnnotatedLists(const Node *relative, CodeMarker *marker, + const NodeMultiMap &nmm) +{ + const auto &uniqueKeys = nmm.uniqueKeys(); + for (const QString &name : uniqueKeys) { + if (!name.isEmpty()) { + out() << "<h2 id=\"" << registerRef(name.toLower()) << "\">" << protectEnc(name) + << "</h2>\n"; + } + generateAnnotatedList(relative, marker, nmm.values(name)); + } +} + +/*! + This function finds the common prefix of the names of all + the classes in the class map \a nmm and then generates a + compact list of the class names alphabetized on the part + of the name not including the common prefix. You can tell + the function to use \a commonPrefix as the common prefix, + but normally you let it figure it out itself by looking at + the name of the first and last classes in the class map + \a nmm. + */ +void HtmlGenerator::generateCompactList(ListType listType, const Node *relative, + const NodeMultiMap &nmm, bool includeAlphabet, + const QString &commonPrefix) +{ + if (nmm.isEmpty()) + return; + + const int NumParagraphs = 37; // '0' to '9', 'A' to 'Z', '_' + qsizetype commonPrefixLen = commonPrefix.size(); + + /* + Divide the data into 37 paragraphs: 0, ..., 9, A, ..., Z, + underscore (_). QAccel will fall in paragraph 10 (A) and + QXtWidget in paragraph 33 (X). This is the only place where we + assume that NumParagraphs is 37. Each paragraph is a NodeMultiMap. + */ + NodeMultiMap paragraph[NumParagraphs + 1]; + QString paragraphName[NumParagraphs + 1]; + QSet<char> usedParagraphNames; + + for (auto c = nmm.constBegin(); c != nmm.constEnd(); ++c) { + QStringList pieces = c.key().split("::"); + int idx = commonPrefixLen; + if (idx > 0 && !pieces.last().startsWith(commonPrefix, Qt::CaseInsensitive)) + idx = 0; + QString last = pieces.last().toLower(); + QString key = last.mid(idx); + + int paragraphNr = NumParagraphs - 1; + + if (key[0].digitValue() != -1) { + paragraphNr = key[0].digitValue(); + } else if (key[0] >= QLatin1Char('a') && key[0] <= QLatin1Char('z')) { + paragraphNr = 10 + key[0].unicode() - 'a'; + } + + paragraphName[paragraphNr] = key[0].toUpper(); + usedParagraphNames.insert(key[0].toLower().cell()); + paragraph[paragraphNr].insert(last, c.value()); + } + + /* + Each paragraph j has a size: paragraph[j].count(). In the + discussion, we will assume paragraphs 0 to 5 will have sizes + 3, 1, 4, 1, 5, 9. + + We now want to compute the paragraph offset. Paragraphs 0 to 6 + start at offsets 0, 3, 4, 8, 9, 14, 23. + */ + qsizetype paragraphOffset[NumParagraphs + 1]; // 37 + 1 + paragraphOffset[0] = 0; + for (int i = 0; i < NumParagraphs; i++) // i = 0..36 + paragraphOffset[i + 1] = paragraphOffset[i] + paragraph[i].size(); + + /* + Output the alphabet as a row of links. + */ + if (includeAlphabet) { + out() << "<p class=\"centerAlign functionIndex\" translate=\"no\"><b>"; + for (int i = 0; i < 26; i++) { + QChar ch('a' + i); + if (usedParagraphNames.contains(char('a' + i))) + out() << QString("<a href=\"#%1\">%2</a> ").arg(ch).arg(ch.toUpper()); + } + out() << "</b></p>\n"; + } + + /* + Output a <div> element to contain all the <dl> elements. + */ + out() << "<div class=\"flowListDiv\" translate=\"no\">\n"; + m_numTableRows = 0; + + int curParNr = 0; + int curParOffset = 0; + QString previousName; + bool multipleOccurrences = false; + + for (int i = 0; i < nmm.size(); i++) { + while ((curParNr < NumParagraphs) && (curParOffset == paragraph[curParNr].size())) { + ++curParNr; + curParOffset = 0; + } + + /* + Starting a new paragraph means starting a new <dl>. + */ + if (curParOffset == 0) { + if (i > 0) + out() << "</dl>\n"; + if (++m_numTableRows % 2 == 1) + out() << "<dl class=\"flowList odd\">"; + else + out() << "<dl class=\"flowList even\">"; + out() << "<dt class=\"alphaChar\""; + if (includeAlphabet) + out() << QString(" id=\"%1\"").arg(paragraphName[curParNr][0].toLower()); + out() << "><b>" << paragraphName[curParNr] << "</b></dt>\n"; + } + + /* + Output a <dd> for the current offset in the current paragraph. + */ + out() << "<dd>"; + if ((curParNr < NumParagraphs) && !paragraphName[curParNr].isEmpty()) { + NodeMultiMap::Iterator it; + NodeMultiMap::Iterator next; + it = paragraph[curParNr].begin(); + for (int j = 0; j < curParOffset; j++) + ++it; + + if (listType == Generic) { + /* + Previously, we used generateFullName() for this, but we + require some special formatting. + */ + out() << "<a href=\"" << linkForNode(it.value(), relative) << "\">"; + } else if (listType == Obsolete) { + QString fileName = fileBase(it.value()) + "-obsolete." + fileExtension(); + QString link; + if (useOutputSubdirs()) + link = "../%1/"_L1.arg(it.value()->tree()->physicalModuleName()); + link += fileName; + out() << "<a href=\"" << link << "\">"; + } + + QStringList pieces{it.value()->fullName(relative).split("::"_L1)}; + const auto &name{pieces.last()}; + next = it; + ++next; + if (name != previousName) + multipleOccurrences = false; + if ((next != paragraph[curParNr].end()) && (name == next.value()->name())) { + multipleOccurrences = true; + previousName = name; + } + if (multipleOccurrences && pieces.size() == 1) + pieces.last().append(": %1"_L1.arg(it.value()->tree()->camelCaseModuleName())); + + out() << protectEnc(pieces.last()); + out() << "</a>"; + if (pieces.size() > 1) { + out() << " ("; + generateFullName(it.value()->parent(), relative); + out() << ')'; + } + } + out() << "</dd>\n"; + curParOffset++; + } + if (nmm.size() > 0) + out() << "</dl>\n"; + + out() << "</div>\n"; +} + +void HtmlGenerator::generateFunctionIndex(const Node *relative) +{ + out() << "<p class=\"centerAlign functionIndex\" translate=\"no\"><b>"; + for (int i = 0; i < 26; i++) { + QChar ch('a' + i); + out() << QString("<a href=\"#%1\">%2</a> ").arg(ch).arg(ch.toUpper()); + } + out() << "</b></p>\n"; + + char nextLetter = 'a'; + + out() << "<ul translate=\"no\">\n"; + NodeMapMap &funcIndex = m_qdb->getFunctionIndex(); + for (auto fnMap = funcIndex.constBegin(); fnMap != funcIndex.constEnd(); ++fnMap) { + const QString &key = fnMap.key(); + const QChar firstLetter = key.isEmpty() ? QChar('A') : key.front(); + Q_ASSERT_X(firstLetter.unicode() < 256, "generateFunctionIndex", + "Only valid C++ identifiers were expected"); + const char currentLetter = firstLetter.isLower() ? firstLetter.unicode() : nextLetter - 1; + + if (currentLetter < nextLetter) { + out() << "<li>"; + } else { + // TODO: This is not covered by our tests + while (nextLetter < currentLetter) + out() << QStringLiteral("<li id=\"%1\"></li>").arg(nextLetter++); + Q_ASSERT(nextLetter == currentLetter); + out() << QStringLiteral("<li id=\"%1\">").arg(nextLetter++); + } + out() << protectEnc(key) << ':'; + + for (auto it = (*fnMap).constBegin(); it != (*fnMap).constEnd(); ++it) { + out() << ' '; + generateFullName((*it)->parent(), relative, *it); + } + out() << "</li>\n"; + } + while (nextLetter <= 'z') + out() << QStringLiteral("<li id=\"%1\"></li>").arg(nextLetter++); + out() << "</ul>\n"; +} + +void HtmlGenerator::generateLegaleseList(const Node *relative, CodeMarker *marker) +{ + TextToNodeMap &legaleseTexts = m_qdb->getLegaleseTexts(); + for (auto it = legaleseTexts.cbegin(), end = legaleseTexts.cend(); it != end; ++it) { + Text text = it.key(); + generateText(text, relative, marker); + out() << "<ul>\n"; + do { + out() << "<li>"; + generateFullName(it.value(), relative); + out() << "</li>\n"; + ++it; + } while (it != legaleseTexts.constEnd() && it.key() == text); + out() << "</ul>\n"; + } +} + +void HtmlGenerator::generateQmlItem(const Node *node, const Node *relative, CodeMarker *marker, + bool summary) +{ + QString marked = marker->markedUpQmlItem(node, summary); + marked.replace("@param>", "i>"); + + marked.replace("<@extra>", "<code class=\"%1 extra\" translate=\"no\">"_L1 + .arg(summary ? "summary"_L1 : "details"_L1)); + marked.replace("</@extra>", "</code>"); + + + if (summary) { + marked.remove("<@name>"); + marked.remove("</@name>"); + marked.remove("<@type>"); + marked.remove("</@type>"); + } + out() << highlightedCode(marked, relative, false, Node::QML); +} + +/*! + This function generates a simple list (without annotations) for + the members of collection node \a {cn}. The list is sorted + according to \a sortOrder. + + Returns \c true if the list was generated (collection has members), + \c false otherwise. + */ +bool HtmlGenerator::generateGroupList(CollectionNode *cn, Qt::SortOrder sortOrder) +{ + m_qdb->mergeCollections(cn); + if (cn->members().isEmpty()) + return false; + + NodeList members{cn->members()}; + if (sortOrder == Qt::DescendingOrder) + std::sort(members.rbegin(), members.rend(), Node::nodeSortKeyOrNameLessThan); + else + std::sort(members.begin(), members.end(), Node::nodeSortKeyOrNameLessThan); + out() << "<ul>\n"; + for (const auto *node : std::as_const(members)) { + out() << "<li translate=\"no\">"; + generateFullName(node, nullptr); + out() << "</li>\n"; + } + out() << "</ul>\n"; + return true; +} + +void HtmlGenerator::generateList(const Node *relative, CodeMarker *marker, + const QString &selector, Qt::SortOrder sortOrder) +{ + CNMap cnm; + Node::NodeType type = Node::NoType; + if (selector == QLatin1String("overviews")) + type = Node::Group; + else if (selector == QLatin1String("cpp-modules")) + type = Node::Module; + else if (selector == QLatin1String("qml-modules")) + type = Node::QmlModule; + if (type != Node::NoType) { + NodeList nodeList; + m_qdb->mergeCollections(type, cnm, relative); + const auto collectionList = cnm.values(); + nodeList.reserve(collectionList.size()); + for (auto *collectionNode : collectionList) + nodeList.append(collectionNode); + generateAnnotatedList(relative, marker, nodeList, sortOrder); + } else { + /* + \generatelist {selector} is only allowed in a + comment where the topic is \group, \module, or + \qmlmodule. + */ + if (relative && !relative->isCollectionNode()) { + relative->doc().location().warning( + QStringLiteral("\\generatelist {%1} is only allowed in \\group, " + "\\module and \\qmlmodule comments.") + .arg(selector)); + return; + } + auto *node = const_cast<Node *>(relative); + auto *collectionNode = static_cast<CollectionNode *>(node); + m_qdb->mergeCollections(collectionNode); + generateAnnotatedList(collectionNode, marker, collectionNode->members(), sortOrder); + } +} + +void HtmlGenerator::generateSection(const NodeVector &nv, const Node *relative, CodeMarker *marker) +{ + bool alignNames = true; + if (!nv.isEmpty()) { + bool twoColumn = false; + if (nv.first()->isProperty()) { + twoColumn = (nv.size() >= 5); + alignNames = false; + } + if (alignNames) { + out() << "<div class=\"table\"><table class=\"alignedsummary\" translate=\"no\">\n"; + } else { + if (twoColumn) + out() << "<div class=\"table\"><table class=\"propsummary\" translate=\"no\">\n" + << "<tr><td class=\"topAlign\">"; + out() << "<ul>\n"; + } + + int i = 0; + for (const auto &member : nv) { + if (member->access() == Access::Private) + continue; + + if (alignNames) { + out() << "<tr><td class=\"memItemLeft rightAlign topAlign\"> "; + } else { + if (twoColumn && i == (nv.size() + 1) / 2) + out() << "</ul></td><td class=\"topAlign\"><ul>\n"; + out() << "<li class=\"fn\" translate=\"no\">"; + } + + generateSynopsis(member, relative, marker, Section::Summary, alignNames); + if (alignNames) + out() << "</td></tr>\n"; + else + out() << "</li>\n"; + i++; + } + if (alignNames) + out() << "</table></div>\n"; + else { + out() << "</ul>\n"; + if (twoColumn) + out() << "</td></tr>\n</table></div>\n"; + } + } +} + +void HtmlGenerator::generateSectionList(const Section §ion, const Node *relative, + CodeMarker *marker, bool useObsoleteMembers) +{ + bool alignNames = true; + const NodeVector &members = + (useObsoleteMembers ? section.obsoleteMembers() : section.members()); + if (!members.isEmpty()) { + bool hasPrivateSignals = false; + bool isInvokable = false; + bool twoColumn = false; + if (section.style() == Section::AllMembers) { + alignNames = false; + twoColumn = (members.size() >= 16); + } else if (members.first()->isProperty()) { + twoColumn = (members.size() >= 5); + alignNames = false; + } + if (alignNames) { + out() << "<div class=\"table\"><table class=\"alignedsummary\" translate=\"no\">\n"; + } else { + if (twoColumn) + out() << "<div class=\"table\"><table class=\"propsummary\" translate=\"no\">\n" + << "<tr><td class=\"topAlign\">"; + out() << "<ul>\n"; + } + + int i = 0; + for (const auto &member : members) { + if (member->access() == Access::Private) + continue; + + if (alignNames) { + out() << "<tr><td class=\"memItemLeft topAlign rightAlign\"> "; + } else { + if (twoColumn && i == (members.size() + 1) / 2) + out() << "</ul></td><td class=\"topAlign\"><ul>\n"; + out() << "<li class=\"fn\" translate=\"no\">"; + } + + generateSynopsis(member, relative, marker, section.style(), alignNames); + if (member->isFunction()) { + const auto *fn = static_cast<const FunctionNode *>(member); + if (fn->isPrivateSignal()) { + hasPrivateSignals = true; + if (alignNames) + out() << "</td><td class=\"memItemRight bottomAlign\">[see note below]"; + } else if (fn->isInvokable()) { + isInvokable = true; + if (alignNames) + out() << "</td><td class=\"memItemRight bottomAlign\">[see note below]"; + } + } + if (alignNames) + out() << "</td></tr>\n"; + else + out() << "</li>\n"; + i++; + } + if (alignNames) + out() << "</table></div>\n"; + else { + out() << "</ul>\n"; + if (twoColumn) + out() << "</td></tr>\n</table></div>\n"; + } + if (alignNames) { + if (hasPrivateSignals) + generateAddendum(relative, Generator::PrivateSignal, marker); + if (isInvokable) + generateAddendum(relative, Generator::Invokable, marker); + } + } + + if (!useObsoleteMembers && section.style() == Section::Summary + && !section.inheritedMembers().isEmpty()) { + out() << "<ul>\n"; + generateSectionInheritedList(section, relative); + out() << "</ul>\n"; + } +} + +void HtmlGenerator::generateSectionInheritedList(const Section §ion, const Node *relative) +{ + const QList<std::pair<Aggregate *, int>> &inheritedMembers = section.inheritedMembers(); + for (const auto &member : inheritedMembers) { + out() << "<li class=\"fn\" translate=\"no\">"; + out() << member.second << ' '; + if (member.second == 1) { + out() << section.singular(); + } else { + out() << section.plural(); + } + out() << " inherited from <a href=\"" << fileName(member.first) << '#' + << Generator::cleanRef(section.title().toLower()) << "\">" + << protectEnc(member.first->plainFullName(relative)) << "</a></li>\n"; + } +} + +void HtmlGenerator::generateSynopsis(const Node *node, const Node *relative, CodeMarker *marker, + Section::Style style, bool alignNames) +{ + QString marked = marker->markedUpSynopsis(node, relative, style); + marked.replace("@param>", "i>"); + + if (style == Section::Summary) { + marked.remove("<@name>"); + marked.remove("</@name>"); + } + + if (style == Section::AllMembers) { + static const QRegularExpression extraRegExp("<@extra>.*</@extra>", + QRegularExpression::InvertedGreedinessOption); + marked.remove(extraRegExp); + } else { + marked.replace("<@extra>", "<code class=\"%1 extra\" translate=\"no\">"_L1 + .arg(style == Section::Summary ? "summary"_L1 : "details"_L1)); + marked.replace("</@extra>", "</code>"); + } + + if (style != Section::Details) { + marked.remove("<@type>"); + marked.remove("</@type>"); + } + + out() << highlightedCode(marked, relative, alignNames); +} + +QString HtmlGenerator::highlightedCode(const QString &markedCode, const Node *relative, + bool alignNames, Node::Genus genus) +{ + QString src = markedCode; + QString html; + html.reserve(src.size()); + QStringView arg; + QStringView par1; + + const QChar charLangle = '<'; + const QChar charAt = '@'; + + static const QString typeTag("type"); + static const QString headerTag("headerfile"); + static const QString funcTag("func"); + static const QString linkTag("link"); + + // replace all <@link> tags: "(<@link node=\"([^\"]+)\">).*(</@link>)" + // replace all <@func> tags: "(<@func target=\"([^\"]*)\">)(.*)(</@func>)" + // replace all "(<@(type|headerfile)(?: +[^>]*)?>)(.*)(</@\\2>)" tags + bool done = false; + for (int i = 0, srcSize = src.size(); i < srcSize;) { + if (src.at(i) == charLangle && src.at(i + 1) == charAt) { + if (alignNames && !done) { + html += QLatin1String("</td><td class=\"memItemRight bottomAlign\">"); + done = true; + } + i += 2; + if (parseArg(src, linkTag, &i, srcSize, &arg, &par1)) { + html += QLatin1String("<b>"); + const Node *n = CodeMarker::nodeForString(par1.toString()); + QString link = linkForNode(n, relative); + addLink(link, arg, &html); + html += QLatin1String("</b>"); + } else if (parseArg(src, funcTag, &i, srcSize, &arg, &par1)) { + const FunctionNode *fn = m_qdb->findFunctionNode(par1.toString(), relative, genus); + QString link = linkForNode(fn, relative); + addLink(link, arg, &html); + par1 = QStringView(); + } else if (parseArg(src, typeTag, &i, srcSize, &arg, &par1)) { + par1 = QStringView(); + const Node *n = m_qdb->findTypeNode(arg.toString(), relative, genus); + html += QLatin1String("<span class=\"type\">"); + if (n && (n->isQmlBasicType())) { + if (relative && (relative->genus() == n->genus() || genus == n->genus())) + addLink(linkForNode(n, relative), arg, &html); + else + html += arg; + } else + addLink(linkForNode(n, relative), arg, &html); + html += QLatin1String("</span>"); + } else if (parseArg(src, headerTag, &i, srcSize, &arg, &par1)) { + par1 = QStringView(); + if (arg.startsWith(QLatin1Char('&'))) + html += arg; + else { + const Node *n = m_qdb->findNodeForInclude(QStringList(arg.toString())); + if (n && n != relative) + addLink(linkForNode(n, relative), arg, &html); + else + html += arg; + } + } else { + html += charLangle; + html += charAt; + } + } else { + html += src.at(i++); + } + } + + // replace all + // "<@comment>" -> "<span class=\"comment\">"; + // "<@preprocessor>" -> "<span class=\"preprocessor\">"; + // "<@string>" -> "<span class=\"string\">"; + // "<@char>" -> "<span class=\"char\">"; + // "<@number>" -> "<span class=\"number\">"; + // "<@op>" -> "<span class=\"operator\">"; + // "<@type>" -> "<span class=\"type\">"; + // "<@name>" -> "<span class=\"name\">"; + // "<@keyword>" -> "<span class=\"keyword\">"; + // "</@(?:comment|preprocessor|string|char|number|op|type|name|keyword)>" -> "</span>" + src = html; + html = QString(); + html.reserve(src.size()); + static const QLatin1String spanTags[] = { + QLatin1String("comment>"), QLatin1String("<span class=\"comment\">"), + QLatin1String("preprocessor>"), QLatin1String("<span class=\"preprocessor\">"), + QLatin1String("string>"), QLatin1String("<span class=\"string\">"), + QLatin1String("char>"), QLatin1String("<span class=\"char\">"), + QLatin1String("number>"), QLatin1String("<span class=\"number\">"), + QLatin1String("op>"), QLatin1String("<span class=\"operator\">"), + QLatin1String("type>"), QLatin1String("<span class=\"type\">"), + QLatin1String("name>"), QLatin1String("<span class=\"name\">"), + QLatin1String("keyword>"), QLatin1String("<span class=\"keyword\">") + }; + int nTags = 9; + // Update the upper bound of k in the following code to match the length + // of the above array. + for (int i = 0, n = src.size(); i < n;) { + if (src.at(i) == QLatin1Char('<')) { + if (src.at(i + 1) == QLatin1Char('@')) { + i += 2; + bool handled = false; + for (int k = 0; k != nTags; ++k) { + const QLatin1String &tag = spanTags[2 * k]; + if (i + tag.size() <= src.size() && tag == QStringView(src).mid(i, tag.size())) { + html += spanTags[2 * k + 1]; + i += tag.size(); + handled = true; + break; + } + } + if (!handled) { + // drop 'our' unknown tags (the ones still containing '@') + while (i < n && src.at(i) != QLatin1Char('>')) + ++i; + ++i; + } + continue; + } else if (src.at(i + 1) == QLatin1Char('/') && src.at(i + 2) == QLatin1Char('@')) { + i += 3; + bool handled = false; + for (int k = 0; k != nTags; ++k) { + const QLatin1String &tag = spanTags[2 * k]; + if (i + tag.size() <= src.size() && tag == QStringView(src).mid(i, tag.size())) { + html += QLatin1String("</span>"); + i += tag.size(); + handled = true; + break; + } + } + if (!handled) { + // drop 'our' unknown tags (the ones still containing '@') + while (i < n && src.at(i) != QLatin1Char('>')) + ++i; + ++i; + } + continue; + } + } + html += src.at(i); + ++i; + } + return html; +} + +void HtmlGenerator::generateLink(const Atom *atom) +{ + Q_ASSERT(m_inLink); + + if (m_linkNode && m_linkNode->isFunction()) { + auto match = XmlGenerator::m_funcLeftParen.match(atom->string()); + if (match.hasMatch()) { + // C++: move () outside of link + qsizetype leftParenLoc = match.capturedStart(1); + out() << protectEnc(atom->string().left(leftParenLoc)); + endLink(); + out() << protectEnc(atom->string().mid(leftParenLoc)); + return; + } + } + out() << protectEnc(atom->string()); +} + +QString HtmlGenerator::protectEnc(const QString &string) +{ + return protect(string); +} + +QString HtmlGenerator::protect(const QString &string) +{ +#define APPEND(x) \ + if (html.isEmpty()) { \ + html = string; \ + html.truncate(i); \ + } \ + html += (x); + + QString html; + qsizetype n = string.size(); + + for (int i = 0; i < n; ++i) { + QChar ch = string.at(i); + + if (ch == QLatin1Char('&')) { + APPEND("&"); + } else if (ch == QLatin1Char('<')) { + APPEND("<"); + } else if (ch == QLatin1Char('>')) { + APPEND(">"); + } else if (ch == QChar(8211)) { + APPEND("–"); + } else if (ch == QChar(8212)) { + APPEND("—"); + } else if (ch == QLatin1Char('"')) { + APPEND("""); + } else { + if (!html.isEmpty()) + html += ch; + } + } + + if (!html.isEmpty()) + return html; + return string; + +#undef APPEND +} + +QString HtmlGenerator::fileBase(const Node *node) const +{ + QString result = Generator::fileBase(node); + if (!node->isAggregate() && node->isDeprecated()) + result += QLatin1String("-obsolete"); + return result; +} + +QString HtmlGenerator::fileName(const Node *node) +{ + if (node->isExternalPage()) + return node->name(); + return Generator::fileName(node); +} + +void HtmlGenerator::generateFullName(const Node *apparentNode, const Node *relative, + const Node *actualNode) +{ + if (actualNode == nullptr) + actualNode = apparentNode; + bool link = !linkForNode(actualNode, relative).isEmpty(); + if (link) { + out() << "<a href=\"" << linkForNode(actualNode, relative); + if (actualNode->isDeprecated()) + out() << "\" class=\"obsolete"; + out() << "\">"; + } + out() << protectEnc(apparentNode->fullName(relative)); + if (link) + out() << "</a>"; +} + +void HtmlGenerator::generateDetailedMember(const Node *node, const PageNode *relative, + CodeMarker *marker) +{ + const EnumNode *etn; + generateExtractionMark(node, MemberMark); + QString nodeRef = nullptr; + if (node->isSharedCommentNode()) { + const auto *scn = reinterpret_cast<const SharedCommentNode *>(node); + const QList<Node *> &collective = scn->collective(); + if (collective.size() > 1) + out() << "<div class=\"fngroup\">\n"; + for (const auto *sharedNode : collective) { + nodeRef = refForNode(sharedNode); + out() << R"(<h3 class="fn fngroupitem" translate="no" id=")" << nodeRef << "\">"; + generateSynopsis(sharedNode, relative, marker, Section::Details); + out() << "</h3>"; + } + if (collective.size() > 1) + out() << "</div>"; + out() << '\n'; + } else { + nodeRef = refForNode(node); + if (node->isEnumType() && (etn = static_cast<const EnumNode *>(node))->flagsType()) { + out() << R"(<h3 class="flags" id=")" << nodeRef << "\">"; + generateSynopsis(etn, relative, marker, Section::Details); + out() << "<br/>"; + generateSynopsis(etn->flagsType(), relative, marker, Section::Details); + out() << "</h3>\n"; + } else { + out() << R"(<h3 class="fn" translate="no" id=")" << nodeRef << "\">"; + generateSynopsis(node, relative, marker, Section::Details); + out() << "</h3>" << '\n'; + } + } + + generateStatus(node, marker); + generateBody(node, marker); + generateOverloadedSignal(node, marker); + generateComparisonCategory(node, marker); + generateThreadSafeness(node, marker); + generateSince(node, marker); + generateNoexceptNote(node, marker); + + if (node->isProperty()) { + const auto property = static_cast<const PropertyNode *>(node); + if (property->propertyType() == PropertyNode::PropertyType::StandardProperty) { + Section section("", "", "", "", Section::Accessors); + + section.appendMembers(property->getters().toVector()); + section.appendMembers(property->setters().toVector()); + section.appendMembers(property->resetters().toVector()); + + if (!section.members().isEmpty()) { + out() << "<p><b>Access functions:</b></p>\n"; + generateSectionList(section, node, marker); + } + + Section notifiers("", "", "", "", Section::Accessors); + notifiers.appendMembers(property->notifiers().toVector()); + + if (!notifiers.members().isEmpty()) { + out() << "<p><b>Notifier signal:</b></p>\n"; + generateSectionList(notifiers, node, marker); + } + } + } else if (node->isEnumType()) { + const auto *enumTypeNode = static_cast<const EnumNode *>(node); + if (enumTypeNode->flagsType()) { + out() << "<p>The " << protectEnc(enumTypeNode->flagsType()->name()) + << " type is a typedef for " + << "<a href=\"" << m_qflagsHref << "\">QFlags</a><" + << protectEnc(enumTypeNode->name()) << ">. It stores an OR combination of " + << protectEnc(enumTypeNode->name()) << " values.</p>\n"; + } + } + generateAlsoList(node, marker); + generateExtractionMark(node, EndMark); +} + +/*! + This version of the function is called when outputting the link + to an example file or example image, where the \a link is known + to be correct. + */ +void HtmlGenerator::beginLink(const QString &link) +{ + m_link = link; + m_inLink = true; + m_linkNode = nullptr; + + if (!m_link.isEmpty()) + out() << "<a href=\"" << m_link << "\" translate=\"no\">"; +} + +void HtmlGenerator::beginLink(const QString &link, const Node *node, const Node *relative) +{ + m_link = link; + m_inLink = true; + m_linkNode = node; + if (m_link.isEmpty()) + return; + + const QString &translate_attr = + (node && node->genus() & Node::API) ? " translate=\"no\""_L1 : ""_L1; + + if (node == nullptr || (relative != nullptr && node->status() == relative->status())) + out() << "<a href=\"" << m_link << "\"%1>"_L1.arg(translate_attr); + else if (node->isDeprecated()) + out() << "<a href=\"" << m_link << "\" class=\"obsolete\"%1>"_L1.arg(translate_attr); + else + out() << "<a href=\"" << m_link << "\"%1>"_L1.arg(translate_attr); +} + +void HtmlGenerator::endLink() +{ + if (!m_inLink) + return; + + m_inLink = false; + m_linkNode = nullptr; + + if (!m_link.isEmpty()) + out() << "</a>"; +} + +/*! + Generates the summary list for the \a members. Only used for + sections of QML element documentation. + */ +void HtmlGenerator::generateQmlSummary(const NodeVector &members, const Node *relative, + CodeMarker *marker) +{ + if (!members.isEmpty()) { + out() << "<ul>\n"; + for (const auto &member : members) { + out() << "<li class=\"fn\" translate=\"no\">"; + generateQmlItem(member, relative, marker, true); + if (member->isPropertyGroup()) { + const auto *scn = static_cast<const SharedCommentNode *>(member); + if (scn->count() > 0) { + out() << "<ul>\n"; + const QList<Node *> &sharedNodes = scn->collective(); + for (const auto &node : sharedNodes) { + if (node->isQmlProperty()) { + out() << "<li class=\"fn\" translate=\"no\">"; + generateQmlItem(node, relative, marker, true); + out() << "</li>\n"; + } + } + out() << "</ul>\n"; + } + } + out() << "</li>\n"; + } + out() << "</ul>\n"; + } +} + +/*! + Outputs the html detailed documentation for a section + on a QML element reference page. + */ +void HtmlGenerator::generateDetailedQmlMember(Node *node, const Aggregate *relative, + CodeMarker *marker) +{ + generateExtractionMark(node, MemberMark); + + QString qmlItemHeader("<div class=\"qmlproto\" translate=\"no\">\n" + "<div class=\"table\"><table class=\"qmlname\">\n"); + + QString qmlItemStart("<tr valign=\"top\" class=\"odd\" id=\"%1\">\n" + "<td class=\"%2\"><p>\n"); + QString qmlItemEnd("</p></td></tr>\n"); + + QString qmlItemFooter("</table></div></div>\n"); + + auto generateQmlProperty = [&](Node *n) { + out() << qmlItemStart.arg(refForNode(n), "tblQmlPropNode"); + generateQmlItem(n, relative, marker, false); + out() << qmlItemEnd; + }; + + auto generateQmlMethod = [&](Node *n) { + out() << qmlItemStart.arg(refForNode(n), "tblQmlFuncNode"); + generateSynopsis(n, relative, marker, Section::Details, false); + out() << qmlItemEnd; + }; + + out() << "<div class=\"qmlitem\">"; + if (node->isPropertyGroup()) { + const auto *scn = static_cast<const SharedCommentNode *>(node); + out() << qmlItemHeader; + if (!scn->name().isEmpty()) { + const QString nodeRef = refForNode(scn); + out() << R"(<tr valign="top" class="even" id=")" << nodeRef << "\">"; + out() << "<th class=\"centerAlign\"><p>"; + out() << "<b>" << scn->name() << " group</b>"; + out() << "</p></th></tr>\n"; + } + const QList<Node *> sharedNodes = scn->collective(); + for (const auto &sharedNode : sharedNodes) { + if (sharedNode->isQmlProperty()) + generateQmlProperty(sharedNode); + } + out() << qmlItemFooter; + } else if (node->isQmlProperty()) { + out() << qmlItemHeader; + generateQmlProperty(node); + out() << qmlItemFooter; + } else if (node->isSharedCommentNode()) { + const auto *scn = reinterpret_cast<const SharedCommentNode *>(node); + const QList<Node *> &sharedNodes = scn->collective(); + if (sharedNodes.size() > 1) + out() << "<div class=\"fngroup\">\n"; + out() << qmlItemHeader; + for (const auto &sharedNode : sharedNodes) { + // Generate the node only if it's a QML method + if (sharedNode->isFunction(Node::QML)) + generateQmlMethod(sharedNode); + else if (sharedNode->isQmlProperty()) + generateQmlProperty(sharedNode); + } + out() << qmlItemFooter; + if (sharedNodes.size() > 1) + out() << "</div>"; // fngroup + } else { // assume the node is a method/signal handler + out() << qmlItemHeader; + generateQmlMethod(node); + out() << qmlItemFooter; + } + + out() << "<div class=\"qmldoc\">"; + generateStatus(node, marker); + generateBody(node, marker); + generateThreadSafeness(node, marker); + generateSince(node, marker); + generateAlsoList(node, marker); + out() << "</div></div>"; + generateExtractionMark(node, EndMark); +} + +void HtmlGenerator::generateExtractionMark(const Node *node, ExtractionMarkType markType) +{ + if (markType != EndMark) { + out() << "<!-- $$$" + node->name(); + if (markType == MemberMark) { + if (node->isFunction()) { + const auto *func = static_cast<const FunctionNode *>(node); + if (!func->hasAssociatedProperties()) { + if (func->overloadNumber() == 0) + out() << "[overload1]"; + out() << "$$$" + func->name() + func->parameters().rawSignature().remove(' '); + } + } else if (node->isProperty()) { + out() << "-prop"; + const auto *prop = static_cast<const PropertyNode *>(node); + const NodeList &list = prop->functions(); + for (const auto *propFuncNode : list) { + if (propFuncNode->isFunction()) { + const auto *func = static_cast<const FunctionNode *>(propFuncNode); + out() << "$$$" + func->name() + + func->parameters().rawSignature().remove(' '); + } + } + } else if (node->isEnumType()) { + const auto *enumNode = static_cast<const EnumNode *>(node); + const auto &items = enumNode->items(); + for (const auto &item : items) + out() << "$$$" + item.name(); + } + } else if (markType == BriefMark) { + out() << "-brief"; + } else if (markType == DetailedDescriptionMark) { + out() << "-description"; + } + out() << " -->\n"; + } else { + out() << "<!-- @@@" + node->name() + " -->\n"; + } +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/htmlgenerator.h b/src/qdoc/qdoc/src/qdoc/htmlgenerator.h new file mode 100644 index 000000000..299240c24 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/htmlgenerator.h @@ -0,0 +1,181 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef HTMLGENERATOR_H +#define HTMLGENERATOR_H + +#include "codemarker.h" +#include "xmlgenerator.h" +#include "filesystem/fileresolver.h" + +#include <QtCore/qhash.h> +#include <QtCore/qregularexpression.h> + +QT_BEGIN_NAMESPACE + +class Aggregate; +class Config; +class ExampleNode; +class HelpProjectWriter; +class ManifestWriter; + +class HtmlGenerator : public XmlGenerator +{ +public: + HtmlGenerator(FileResolver& file_resolver); + ~HtmlGenerator() override; + + void initializeGenerator() override; + void terminateGenerator() override; + QString format() override; + void generateDocs() override; + + QString protectEnc(const QString &string); + static QString protect(const QString &string); + +protected: + void generateExampleFilePage(const Node *en, ResolvedFile resolved_file, CodeMarker *marker) override; + qsizetype generateAtom(const Atom *atom, const Node *relative, CodeMarker *marker) override; + void generateCppReferencePage(Aggregate *aggregate, CodeMarker *marker) override; + void generateProxyPage(Aggregate *aggregate, CodeMarker *marker) override; + void generateQmlTypePage(QmlTypeNode *qcn, CodeMarker *marker) override; + void generatePageNode(PageNode *pn, CodeMarker *marker) override; + void generateCollectionNode(CollectionNode *cn, CodeMarker *marker) override; + void generateGenericCollectionPage(CollectionNode *cn, CodeMarker *marker) override; + [[nodiscard]] QString fileExtension() const override; + +private: + enum SubTitleSize { SmallSubTitle, LargeSubTitle }; + enum ExtractionMarkType { BriefMark, DetailedDescriptionMark, MemberMark, EndMark }; + + void generateNavigationBar(const QString &title, const Node *node, CodeMarker *marker, + const QString &buildversion, bool tableItems = false); + void generateHeader(const QString &title, const Node *node = nullptr, + CodeMarker *marker = nullptr); + void generateTitle(const QString &title, const Text &subTitle, SubTitleSize subTitleSize, + const Node *relative, CodeMarker *marker); + void generateFooter(const Node *node = nullptr); + void generateRequisites(Aggregate *inner, CodeMarker *marker); + void generateQmlRequisites(QmlTypeNode *qcn, CodeMarker *marker); + void generateBrief(const Node *node, CodeMarker *marker, const Node *relative = nullptr, + bool addLink = true); + void generateTableOfContents(const Node *node, CodeMarker *marker, + QList<Section> *sections = nullptr); + void generateSidebar(); + QString generateAllMembersFile(const Section §ion, CodeMarker *marker); + QString generateAllQmlMembersFile(const Sections §ions, CodeMarker *marker); + QString generateObsoleteMembersFile(const Sections §ions, CodeMarker *marker); + QString generateObsoleteQmlMembersFile(const Sections §ions, CodeMarker *marker); + void generateClassHierarchy(const Node *relative, NodeMultiMap &classMap); + void generateAnnotatedLists(const Node *relative, CodeMarker *marker, + const NodeMultiMap &nodeMap); + void generateAnnotatedList(const Node *relative, CodeMarker *marker, const NodeList &nodes, + Qt::SortOrder sortOrder = Qt::AscendingOrder); + void generateCompactList(ListType listType, const Node *relative, const NodeMultiMap &classMap, + bool includeAlphabet, const QString &commonPrefix); + void generateFunctionIndex(const Node *relative); + void generateLegaleseList(const Node *relative, CodeMarker *marker); + bool generateGroupList(CollectionNode *cn, Qt::SortOrder sortOrder = Qt::AscendingOrder); + void generateList(const Node *relative, CodeMarker *marker, const QString &selector, + Qt::SortOrder sortOrder = Qt::AscendingOrder); + void generateSectionList(const Section §ion, const Node *relative, CodeMarker *marker, + bool useObsoloteMembers = false); + void generateQmlSummary(const NodeVector &members, const Node *relative, CodeMarker *marker); + void generateQmlItem(const Node *node, const Node *relative, CodeMarker *marker, bool summary); + void generateDetailedQmlMember(Node *node, const Aggregate *relative, CodeMarker *marker); + + void generateSection(const NodeVector &nv, const Node *relative, CodeMarker *marker); + void generateSynopsis(const Node *node, const Node *relative, CodeMarker *marker, + Section::Style style, bool alignNames = false); + void generateSectionInheritedList(const Section §ion, const Node *relative); + QString highlightedCode(const QString &markedCode, const Node *relative, + bool alignNames = false, Node::Genus genus = Node::DontCare); + + void generateFullName(const Node *apparentNode, const Node *relative, + const Node *actualNode = nullptr); + void generateDetailedMember(const Node *node, const PageNode *relative, CodeMarker *marker); + void generateLink(const Atom *atom); + + QString fileBase(const Node *node) const override; + QString fileName(const Node *node); + + void beginLink(const QString &link); + void beginLink(const QString &link, const Node *node, const Node *relative); + void endLink(); + void generateExtractionMark(const Node *node, ExtractionMarkType markType); + void addIncludeFileToMap(const Aggregate *aggregate, CodeMarker *marker, + QMap<QString, Text> &requisites, Text& text, + const QString &headerText); + void addSinceToMap(const Aggregate *aggregate, QMap<QString, Text> &requisites, Text *text, + const QString &sinceText) const; + void addStatusToMap(const Aggregate *aggregate, QMap<QString, Text> &requisites, Text &text, + const QString &statusText) const; + void addCMakeInfoToMap(const Aggregate *aggregate, QMap<QString, Text> &requisites, Text *text, + const QString &CMakeInfo) const; + void addQtVariableToMap(const Aggregate *aggregate, QMap<QString, Text> &requisites, Text *text, + const QString &qtVariableText) const; + void addQmlNativeTypesToMap(QMap<QString, Text> &requisites, Text *text, + const QString &nativeTypeText, + ClassNode *classe) const; + void addInheritsToMap(QMap<QString, Text> &requisites, Text *text, const QString &inheritsText, + ClassNode *classe); + void addInheritedByToMap(QMap<QString, Text> &requisites, Text *text, + const QString &inheritedBytext, ClassNode *classe); + void generateTheTable(const QStringList &requisiteOrder, const QMap<QString, Text> &requisites, + const QString &headerText, const Aggregate *aggregate, + CodeMarker *marker); + inline void openUnorderedList(); + inline void closeUnorderedList(); + + QString groupReferenceText(PageNode* node); + + static bool s_inUnorderedList; + + int m_codeIndent { 0 }; + QString m_codePrefix {}; + QString m_codeSuffix {}; + HelpProjectWriter *m_helpProjectWriter { nullptr }; + ManifestWriter *m_manifestWriter { nullptr }; + QString m_headerScripts {}; + QString m_headerStyles {}; + QString m_endHeader {}; + QString m_postHeader {}; + QString m_postPostHeader {}; + QString m_prologue {}; + QString m_footer {}; + QString m_address {}; + bool m_noNavigationBar { false }; + QString m_project {}; + QString m_projectDescription {}; + QString m_projectUrl {}; + QString m_navigationLinks {}; + QString m_navigationSeparator {}; + QString m_homepage {}; + QString m_hometitle {}; + QString m_landingpage {}; + QString m_landingtitle {}; + QString m_cppclassespage {}; + QString m_cppclassestitle {}; + QString m_qmltypespage {}; + QString m_qmltypestitle {}; + QString m_trademarkspage {}; + QString m_buildversion {}; + QString m_qflagsHref {}; + int tocDepth {}; + + Config *config { nullptr }; + +}; + +#define HTMLGENERATOR_ADDRESS "address" +#define HTMLGENERATOR_FOOTER "footer" +#define HTMLGENERATOR_POSTHEADER "postheader" +#define HTMLGENERATOR_POSTPOSTHEADER "postpostheader" +#define HTMLGENERATOR_PROLOGUE "prologue" +#define HTMLGENERATOR_NONAVIGATIONBAR "nonavigationbar" +#define HTMLGENERATOR_NAVIGATIONSEPARATOR "navigationseparator" +#define HTMLGENERATOR_TOCDEPTH "tocdepth" + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/importrec.h b/src/qdoc/qdoc/src/qdoc/importrec.h new file mode 100644 index 000000000..84f8f35ac --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/importrec.h @@ -0,0 +1,33 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef IMPORTREC_H +#define IMPORTREC_H + +#include <QtCore/qglobal.h> +#include <QtCore/qstring.h> + +#include <utility> + +QT_BEGIN_NAMESPACE + +struct ImportRec +{ + QString m_moduleName {}; + QString m_majorMinorVersion {}; + QString m_importUri {}; // subdirectory of module directory + + ImportRec(QString name, QString version, QString importUri) + : m_moduleName(std::move(name)), + m_majorMinorVersion(std::move(version)), + m_importUri(std::move(importUri)) + { + } + QString &name() { return m_moduleName; } + QString &version() { return m_majorMinorVersion; } + [[nodiscard]] bool isEmpty() const { return m_moduleName.isEmpty(); } +}; + +QT_END_NAMESPACE + +#endif // IMPORTREC_H diff --git a/src/qdoc/qdoc/src/qdoc/location.cpp b/src/qdoc/qdoc/src/qdoc/location.cpp new file mode 100644 index 000000000..714e232d7 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/location.cpp @@ -0,0 +1,423 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "location.h" + +#include "config.h" + +#include <QtCore/qdebug.h> +#include <QtCore/qdir.h> +#include <QtCore/qregularexpression.h> + +#include <climits> +#include <cstdio> +#include <cstdlib> + +QT_BEGIN_NAMESPACE + +int Location::s_tabSize; +int Location::s_warningCount = 0; +int Location::s_warningLimit = -1; +QString Location::s_programName; +QString Location::s_project; +QRegularExpression *Location::s_spuriousRegExp = nullptr; + +/*! + \class Location + + \brief The Location class provides a way to mark a location in a file. + + It maintains a stack of file positions. A file position + consists of the file path, line number, and column number. + The location is used for printing error messages that are + tied to a location in a file. + */ + +/*! + Constructs an empty location. + */ +Location::Location() : m_stk(nullptr), m_stkTop(&m_stkBottom), m_stkDepth(0), m_etc(false) +{ + // nothing. +} + +/*! + Constructs a location with (fileName, 1, 1) on its file + position stack. + */ +Location::Location(const QString &fileName) + : m_stk(nullptr), m_stkTop(&m_stkBottom), m_stkDepth(0), m_etc(false) +{ + push(fileName); +} + +/*! + The copy constructor copies the contents of \a other into + this Location using the assignment operator. + */ +Location::Location(const Location &other) + : m_stk(nullptr), m_stkTop(&m_stkBottom), m_stkDepth(0), m_etc(false) +{ + *this = other; +} + +/*! + The assignment operator does a deep copy of the entire + state of \a other into this Location. + */ +Location &Location::operator=(const Location &other) +{ + if (this == &other) + return *this; + + QStack<StackEntry> *oldStk = m_stk; + + m_stkBottom = other.m_stkBottom; + if (other.m_stk == nullptr) { + m_stk = nullptr; + m_stkTop = &m_stkBottom; + } else { + m_stk = new QStack<StackEntry>(*other.m_stk); + m_stkTop = &m_stk->top(); + } + m_stkDepth = other.m_stkDepth; + m_etc = other.m_etc; + delete oldStk; + return *this; +} + +/*! + If the file position on top of the stack has a line number + less than 1, set its line number to 1 and its column number + to 1. Otherwise, do nothing. + */ +void Location::start() +{ + if (m_stkTop->m_lineNo < 1) { + m_stkTop->m_lineNo = 1; + m_stkTop->m_columnNo = 1; + } +} + +/*! + Advance the current file position, using \a ch to decide how to do + that. If \a ch is a \c{'\\n'}, increment the current line number and + set the column number to 1. If \ch is a \c{'\\t'}, increment to the + next tab column. Otherwise, increment the column number by 1. + + The current file position is the one on top of the position stack. + */ +void Location::advance(QChar ch) +{ + if (ch == QLatin1Char('\n')) { + m_stkTop->m_lineNo++; + m_stkTop->m_columnNo = 1; + } else if (ch == QLatin1Char('\t')) { + m_stkTop->m_columnNo = 1 + s_tabSize * (m_stkTop->m_columnNo + s_tabSize - 1) / s_tabSize; + } else { + m_stkTop->m_columnNo++; + } +} + +/*! + Pushes \a filePath onto the file position stack. The current + file position becomes (\a filePath, 1, 1). + + \sa pop() +*/ +void Location::push(const QString &filePath) +{ + if (m_stkDepth++ >= 1) { + if (m_stk == nullptr) + m_stk = new QStack<StackEntry>; + m_stk->push(StackEntry()); + m_stkTop = &m_stk->top(); + } + + m_stkTop->m_filePath = filePath; + m_stkTop->m_lineNo = INT_MIN; + m_stkTop->m_columnNo = 1; +} + +/*! + Pops the top of the internal stack. The current file position + becomes the next one in the new top of stack. + + \sa push() +*/ +void Location::pop() +{ + if (--m_stkDepth == 0) { + m_stkBottom = StackEntry(); + } else { + if (!m_stk) + return; + m_stk->pop(); + if (m_stk->isEmpty()) { + delete m_stk; + m_stk = nullptr; + m_stkTop = &m_stkBottom; + } else { + m_stkTop = &m_stk->top(); + } + } +} + +/*! \fn bool Location::isEmpty() const + + Returns \c true if there is no file name set yet; returns \c false + otherwise. The functions filePath(), lineNo() and columnNo() + must not be called on an empty Location object. + */ + +/*! \fn const QString &Location::filePath() const + Returns the current path and file name. If the Location is + empty, the returned string is null. + + \sa lineNo(), columnNo() + */ + +/*! + Returns the file name part of the file path, ie the current + file. Returns an empty string if the file path is empty. + */ +QString Location::fileName() const +{ + QFileInfo fi(filePath()); + return fi.fileName(); +} + +/*! + Returns the suffix of the file name. Returns an empty string + if the file path is empty. + */ +QString Location::fileSuffix() const +{ + QString fp = filePath(); + return (fp.isEmpty() ? fp : fp.mid(fp.lastIndexOf('.') + 1)); +} + +/*! \fn int Location::lineNo() const + Returns the current line number. + Must not be called on an empty Location object. + + \sa filePath(), columnNo() +*/ + +/*! \fn int Location::columnNo() const + Returns the current column number. + Must not be called on an empty Location object. + + \sa filePath(), lineNo() +*/ + +/*! + Writes \a message and \a details to stderr as a formatted + warning message. Does not write the message if qdoc is in + the Prepare phase. + */ +void Location::warning(const QString &message, const QString &details) const +{ + const auto &config = Config::instance(); + if (!config.preparing() || config.singleExec()) + emitMessage(Warning, message, details); +} + +/*! + Writes \a message and \a details to stderr as a formatted + error message. Does not write the message if qdoc is in + the Prepare phase. + */ +void Location::error(const QString &message, const QString &details) const +{ + const auto &config = Config::instance(); + if (!config.preparing() || config.singleExec()) + emitMessage(Error, message, details); +} + +/*! + Returns the error code QDoc should exit with; EXIT_SUCCESS + or the number of documentation warnings if they exceeded + the limit set by warninglimit configuration variable. + */ +int Location::exitCode() +{ + if (s_warningLimit < 0 || s_warningCount <= s_warningLimit) + return EXIT_SUCCESS; + + Location().emitMessage( + Error, + QStringLiteral("Documentation warnings (%1) exceeded the limit (%2) for '%3'.") + .arg(QString::number(s_warningCount), QString::number(s_warningLimit), + s_project), + QString()); + return s_warningCount; +} + +/*! + Writes \a message and \a details to stderr as a formatted + error message and then exits the program. qdoc prints fatal + errors in either phase (Prepare or Generate). + */ +void Location::fatal(const QString &message, const QString &details) const +{ + emitMessage(Error, message, details); + information(message); + information(details); + information("Aborting"); + exit(EXIT_FAILURE); +} + +/*! + Writes \a message and \a details to stderr as a formatted + report message. + */ +void Location::report(const QString &message, const QString &details) const +{ + emitMessage(Report, message, details); +} + +/*! + Gets several parameters from the config, including + tab size, program name, and a regular expression that + appears to be used for matching certain error messages + so that emitMessage() can avoid printing them. + */ +void Location::initialize() +{ + Config &config = Config::instance(); + s_tabSize = config.get(CONFIG_TABSIZE).asInt(); + s_programName = config.programName(); + s_project = config.get(CONFIG_PROJECT).asString(); + if (!config.singleExec()) + s_warningCount = 0; + if (qEnvironmentVariableIsSet("QDOC_ENABLE_WARNINGLIMIT") + || config.get(CONFIG_WARNINGLIMIT + Config::dot + "enabled").asBool()) + s_warningLimit = config.get(CONFIG_WARNINGLIMIT).asInt(); + + QRegularExpression regExp = config.getRegExp(CONFIG_SPURIOUS); + if (regExp.isValid()) { + s_spuriousRegExp = new QRegularExpression(regExp); + } else { + config.get(CONFIG_SPURIOUS).location() + .warning(QStringLiteral("Invalid regular expression '%1'") + .arg(regExp.pattern())); + } +} + +/*! + Apparently, all this does is delete the regular expression + used for intercepting certain error messages that should + not be emitted by emitMessage(). + */ +void Location::terminate() +{ + delete s_spuriousRegExp; + s_spuriousRegExp = nullptr; +} + +/*! + Prints \a message to \c stdout followed by a \c{'\n'}. + */ +void Location::information(const QString &message) +{ + printf("%s\n", message.toLatin1().data()); + fflush(stdout); +} + +/*! + Report a program bug, including the \a hint. + */ +void Location::internalError(const QString &hint) +{ + Location().fatal(QStringLiteral("Internal error (%1)").arg(hint), + QStringLiteral("There is a bug in %1. Seek advice from your local" + " %2 guru.") + .arg(s_programName, s_programName)); +} + +/*! + Formats \a message and \a details into a single string + and outputs that string to \c stderr. \a type specifies + whether the \a message is an error or a warning. + */ +void Location::emitMessage(MessageType type, const QString &message, const QString &details) const +{ + if (type == Warning && s_spuriousRegExp != nullptr) { + auto match = s_spuriousRegExp->match(message, 0, QRegularExpression::NormalMatch, + QRegularExpression::AnchorAtOffsetMatchOption); + if (match.hasMatch() && match.capturedLength() == message.size()) + return; + } + + QString result = message; + if (!details.isEmpty()) + result += "\n[" + details + QLatin1Char(']'); + result.replace("\n", "\n "); + if (isEmpty()) { + if (type == Error) + result.prepend(QStringLiteral(": error: ")); + else if (type == Warning) { + result.prepend(QStringLiteral(": warning: ")); + ++s_warningCount; + } + } else { + if (type == Error) + result.prepend(QStringLiteral(": (qdoc) error: ")); + else if (type == Warning) { + result.prepend(QStringLiteral(": (qdoc) warning: ")); + ++s_warningCount; + } + } + if (type != Report) + result.prepend(toString()); + fprintf(stderr, "%s\n", result.toLatin1().data()); + fflush(stderr); +} + +/*! + Converts the location to a string to be prepended to error + messages. + */ +QString Location::toString() const +{ + QString str; + + if (isEmpty()) { + str = s_programName; + } else { + Location loc2 = *this; + loc2.setEtc(false); + loc2.pop(); + if (!loc2.isEmpty()) { + QString blah = QStringLiteral("In file included from "); + for (;;) { + str += blah; + str += loc2.top(); + loc2.pop(); + if (loc2.isEmpty()) + break; + str += QStringLiteral(",\n"); + blah.fill(' '); + } + str += QStringLiteral(":\n"); + } + str += top(); + } + return str; +} + +QString Location::top() const +{ + QDir path(filePath()); + QString str = path.absolutePath(); + if (lineNo() >= 1) { + str += QLatin1Char(':'); + str += QString::number(lineNo()); + } + if (etc()) + str += QLatin1String(" (etc.)"); + return str; +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/location.h b/src/qdoc/qdoc/src/qdoc/location.h new file mode 100644 index 000000000..8427bc917 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/location.h @@ -0,0 +1,92 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef LOCATION_H +#define LOCATION_H + +#include <QtCore/qcoreapplication.h> +#include <QtCore/qstack.h> + +QT_BEGIN_NAMESPACE + +class QRegularExpression; + +class Location +{ +public: + Location(); + explicit Location(const QString &filePath); + Location(const Location &other); + ~Location() { delete m_stk; } + + Location &operator=(const Location &other); + + void start(); + void advance(QChar ch); + void advanceLines(int n) + { + m_stkTop->m_lineNo += n; + m_stkTop->m_columnNo = 1; + } + + void push(const QString &filePath); + void pop(); + void setEtc(bool etc) { m_etc = etc; } + void setLineNo(int no) { m_stkTop->m_lineNo = no; } + void setColumnNo(int no) { m_stkTop->m_columnNo = no; } + + [[nodiscard]] bool isEmpty() const { return m_stkDepth == 0; } + [[nodiscard]] int depth() const { return m_stkDepth; } + [[nodiscard]] const QString &filePath() const { return m_stkTop->m_filePath; } + [[nodiscard]] QString fileName() const; + [[nodiscard]] QString fileSuffix() const; + [[nodiscard]] int lineNo() const { return m_stkTop->m_lineNo; } + [[nodiscard]] int columnNo() const { return m_stkTop->m_columnNo; } + [[nodiscard]] bool etc() const { return m_etc; } + [[nodiscard]] QString toString() const; + void warning(const QString &message, const QString &details = QString()) const; + void error(const QString &message, const QString &details = QString()) const; + void fatal(const QString &message, const QString &details = QString()) const; + void report(const QString &message, const QString &details = QString()) const; + + static void initialize(); + + static void terminate(); + static void information(const QString &message); + static void internalError(const QString &hint); + static int exitCode(); + +private: + enum MessageType { Warning, Error, Report }; + + struct StackEntry + { + QString m_filePath {}; + int m_lineNo {}; + int m_columnNo {}; + }; + friend class QTypeInfo<StackEntry>; + + void emitMessage(MessageType type, const QString &message, const QString &details) const; + [[nodiscard]] QString top() const; + +private: + StackEntry m_stkBottom {}; + QStack<StackEntry> *m_stk {}; + StackEntry *m_stkTop {}; + int m_stkDepth {}; + bool m_etc {}; + + static int s_tabSize; + static int s_warningCount; + static int s_warningLimit; + static QString s_programName; + static QString s_project; + static QRegularExpression *s_spuriousRegExp; +}; +Q_DECLARE_TYPEINFO(Location::StackEntry, Q_RELOCATABLE_TYPE); +Q_DECLARE_TYPEINFO(Location, Q_COMPLEX_TYPE); // stkTop = &stkBottom + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/macro.h b/src/qdoc/qdoc/src/qdoc/macro.h new file mode 100644 index 000000000..11e77fa29 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/macro.h @@ -0,0 +1,27 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +#ifndef MACRO_H +#define MACRO_H + +#include "location.h" + +#include <QtCore/qmap.h> +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +/*! + * Simple structure used by the Doc and DocParser classes. + */ +struct Macro +{ +public: + QString m_defaultDef {}; + Location m_defaultDefLocation {}; + QMap<QString, QString> m_otherDefs {}; + int numParams {}; +}; + +QT_END_NAMESPACE + +#endif // MACRO_H diff --git a/src/qdoc/qdoc/src/qdoc/main.cpp b/src/qdoc/qdoc/src/qdoc/main.cpp new file mode 100644 index 000000000..5e48a6a8a --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/main.cpp @@ -0,0 +1,720 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "clangcodeparser.h" +#include "codemarker.h" +#include "codeparser.h" +#include "config.h" +#include "cppcodemarker.h" +#include "doc.h" +#include "docbookgenerator.h" +#include "htmlgenerator.h" +#include "location.h" +#include "puredocparser.h" +#include "qdocdatabase.h" +#include "qmlcodemarker.h" +#include "qmlcodeparser.h" +#include "sourcefileparser.h" +#include "utilities.h" +#include "tokenizer.h" +#include "tree.h" +#include "webxmlgenerator.h" + +#include "filesystem/fileresolver.h" +#include "boundaries/filesystem/directorypath.h" + +#include <QtCore/qcompilerdetection.h> +#include <QtCore/qdatetime.h> +#include <QtCore/qdebug.h> +#include <QtCore/qglobal.h> +#include <QtCore/qhashfunctions.h> + +#include <set> + +#ifndef QT_BOOTSTRAPPED +# include <QtCore/qcoreapplication.h> +#endif + +#include <algorithm> +#include <cstdlib> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +bool creationTimeBefore(const QFileInfo &fi1, const QFileInfo &fi2) +{ + return fi1.lastModified() < fi2.lastModified(); +} + +/*! + \internal + Inspects each file path in \a sources. File paths with a known + source file type are parsed to extract user-provided + documentation and information about source code level elements. + + \note Unknown source file types are silently ignored. + + The validity or availability of the file paths may or may not cause QDoc + to generate warnings; this depends on the implementation of + parseSourceFile() for the relevant parser. + + \sa CodeParser::parserForSourceFile, CodeParser::sourceFileNameFilter +*/ +static void parseSourceFiles( + std::vector<QString>&& sources, + SourceFileParser& source_file_parser, + CppCodeParser& cpp_code_parser +) { + ParserErrorHandler error_handler{}; + std::stable_sort(sources.begin(), sources.end()); + + sources.erase ( + std::unique(sources.begin(), sources.end()), + sources.end() + ); + + auto qml_sources = + std::stable_partition(sources.begin(), sources.end(), [](const QString& source){ + return CodeParser::parserForSourceFile(source) == CodeParser::parserForLanguage("QML"); + }); + + + std::for_each(qml_sources, sources.end(), + [&source_file_parser, &cpp_code_parser, &error_handler](const QString& source){ + qCDebug(lcQdoc, "Parsing %s", qPrintable(source)); + + auto [untied_documentation, tied_documentation] = source_file_parser(tag_source_file(source)); + std::vector<FnMatchError> errors{}; + + for (auto untied : untied_documentation) { + auto result = cpp_code_parser.processTopicArgs(untied); + tied_documentation.insert(tied_documentation.end(), result.first.begin(), result.first.end()); + }; + + cpp_code_parser.processMetaCommands(tied_documentation); + + // Process errors that occurred during parsing + for (const auto &e : errors) + error_handler(e); + }); + + std::for_each(sources.begin(), qml_sources, [&cpp_code_parser](const QString& source){ + auto *codeParser = CodeParser::parserForSourceFile(source); + if (!codeParser) return; + + qCDebug(lcQdoc, "Parsing %s", qPrintable(source)); + codeParser->parseSourceFile(Config::instance().location(), source, cpp_code_parser); + }); + +} + +/*! + Read some XML indexes containing definitions from other + documentation sets. \a config contains a variable that + lists directories where index files can be found. It also + contains the \c depends variable, which lists the modules + that the current module depends on. \a formats contains + a list of output formats; each format may have a different + output subdirectory where index files are located. +*/ +static void loadIndexFiles(const QSet<QString> &formats) +{ + Config &config = Config::instance(); + QDocDatabase *qdb = QDocDatabase::qdocDB(); + QStringList indexFiles; + const QStringList configIndexes{config.get(CONFIG_INDEXES).asStringList()}; + bool warn = !config.get(CONFIG_NOLINKERRORS).asBool(); + + for (const auto &index : configIndexes) { + QFileInfo fi(index); + if (fi.exists() && fi.isFile()) + indexFiles << index; + else if (warn) + Location().warning(QString("Index file not found: %1").arg(index)); + } + + config.dependModules() += config.get(CONFIG_DEPENDS).asStringList(); + config.dependModules().removeDuplicates(); + bool useNoSubDirs = false; + QSet<QString> subDirs; + + // Add format-specific output subdirectories to the set of + // subdirectories where we look for index files + for (const auto &format : formats) { + if (config.get(format + Config::dot + "nosubdirs").asBool()) { + useNoSubDirs = true; + const auto singleOutputSubdir{QDir(config.getOutputDir(format)).dirName()}; + if (!singleOutputSubdir.isEmpty()) + subDirs << singleOutputSubdir; + } + } + + if (!config.dependModules().empty()) { + if (!config.indexDirs().empty()) { + for (auto &dir : config.indexDirs()) { + if (dir.startsWith("..")) { + const QString prefix(QDir(config.currentDir()) + .relativeFilePath(config.previousCurrentDir())); + if (!prefix.isEmpty()) + dir.prepend(prefix + QLatin1Char('/')); + } + } + /* + Load all dependencies: + Either add all subdirectories of the indexdirs as dependModules, + when an asterisk is used in the 'depends' list, or + when <format>.nosubdirs is set, we need to look for all .index files + in the output subdirectory instead. + */ + bool asteriskUsed = false; + if (config.dependModules().contains("*")) { + config.dependModules().removeOne("*"); + asteriskUsed = true; + if (useNoSubDirs) { + std::for_each(formats.begin(), formats.end(), [&](const QString &format) { + QDir scanDir(config.getOutputDir(format)); + QStringList foundModules = + scanDir.entryList(QStringList("*.index"), QDir::Files); + std::transform( + foundModules.begin(), foundModules.end(), foundModules.begin(), + [](const QString &index) { return QFileInfo(index).baseName(); }); + config.dependModules() << foundModules; + }); + } else { + for (const auto &indexDir : config.indexDirs()) { + QDir scanDir = QDir(indexDir); + scanDir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); + QFileInfoList dirList = scanDir.entryInfoList(); + for (const auto &dir : dirList) + config.dependModules().append(dir.fileName()); + } + } + // Remove self-dependencies and possible duplicates + QString project{config.get(CONFIG_PROJECT).asString()}; + config.dependModules().removeAll(project.toLower()); + config.dependModules().removeDuplicates(); + qCCritical(lcQdoc) << "Configuration file for" + << project << "has depends = *; loading all" + << config.dependModules().size() + << "index files found"; + } + for (const auto &module : config.dependModules()) { + QList<QFileInfo> foundIndices; + // Always look in module-specific subdir, even with *.nosubdirs config + bool useModuleSubDir = !subDirs.contains(module); + subDirs << module; + + for (const auto &dir : config.indexDirs()) { + for (const auto &subDir : std::as_const(subDirs)) { + QString fileToLookFor = dir + QLatin1Char('/') + subDir + QLatin1Char('/') + + module + ".index"; + if (QFile::exists(fileToLookFor)) { + QFileInfo tempFileInfo(fileToLookFor); + if (!foundIndices.contains(tempFileInfo)) + foundIndices.append(tempFileInfo); + } + } + } + // Clear the temporary module-specific subdir + if (useModuleSubDir) + subDirs.remove(module); + std::sort(foundIndices.begin(), foundIndices.end(), creationTimeBefore); + QString indexToAdd; + if (foundIndices.size() > 1) { + /* + QDoc should always use the last entry in the multimap when there are + multiple index files for a module, since the last modified file has the + highest UNIX timestamp. + */ + QStringList indexPaths; + indexPaths.reserve(foundIndices.size()); + for (const auto &found : std::as_const(foundIndices)) + indexPaths << found.absoluteFilePath(); + if (warn) { + Location().warning( + QString("Multiple index files found for dependency \"%1\":\n%2") + .arg(module, indexPaths.join('\n'))); + Location().warning( + QString("Using %1 as index file for dependency \"%2\"") + .arg(foundIndices[foundIndices.size() - 1].absoluteFilePath(), + module)); + } + indexToAdd = foundIndices[foundIndices.size() - 1].absoluteFilePath(); + } else if (foundIndices.size() == 1) { + indexToAdd = foundIndices[0].absoluteFilePath(); + } + if (!indexToAdd.isEmpty()) { + if (!indexFiles.contains(indexToAdd)) + indexFiles << indexToAdd; + } else if (!asteriskUsed && warn) { + Location().warning( + QString(R"("%1" Cannot locate index file for dependency "%2")") + .arg(config.get(CONFIG_PROJECT).asString(), module)); + } + } + } else if (warn) { + Location().warning( + QLatin1String("Dependent modules specified, but no index directories were set. " + "There will probably be errors for missing links.")); + } + } + qdb->readIndexes(indexFiles); +} + +/*! + \internal + Prints to stderr the name of the project that QDoc is running for, + in which mode and which phase. + + If QDoc is not running in debug mode or --log-progress command line + option is not set, do nothing. + */ +void logStartEndMessage(const QLatin1String &startStop, Config &config) +{ + if (!config.get(CONFIG_LOGPROGRESS).asBool()) + return; + + const QString runName = " qdoc for " + + config.get(CONFIG_PROJECT).asString() + + QLatin1String(" in ") + + QLatin1String(config.singleExec() ? "single" : "dual") + + QLatin1String(" process mode: ") + + QLatin1String(config.preparing() ? "prepare" : "generate") + + QLatin1String(" phase."); + + const QString msg = startStop + runName; + qCInfo(lcQdoc) << msg.toUtf8().data(); +} + +/*! + Processes the qdoc config file \a fileName. This is the controller for all + of QDoc. The \a config instance represents the configuration data for QDoc. + All other classes are initialized with the same config. + */ +static void processQdocconfFile(const QString &fileName) +{ + Config &config = Config::instance(); + config.setPreviousCurrentDir(QDir::currentPath()); + + /* + With the default configuration values in place, load + the qdoc configuration file. Note that the configuration + file may include other configuration files. + + The Location class keeps track of the current location + in the file being processed, mainly for error reporting + purposes. + */ + Location::initialize(); + config.load(fileName); + QString project{config.get(CONFIG_PROJECT).asString()}; + if (project.isEmpty()) { + qCCritical(lcQdoc) << QLatin1String("qdoc can't run; no project set in qdocconf file"); + exit(1); + } + Location::terminate(); + + config.setCurrentDir(QFileInfo(fileName).path()); + if (!config.currentDir().isEmpty()) + QDir::setCurrent(config.currentDir()); + + logStartEndMessage(QLatin1String("Start"), config); + + if (config.getDebug()) { + Utilities::startDebugging(QString("command line")); + qCDebug(lcQdoc).noquote() << "Arguments:" << QCoreApplication::arguments(); + } + + // <<TODO: [cleanup-temporary-kludges] + // The underlying validation should be performed at the + // configuration level during parsing. + // This cannot be done straightforwardly with how the Config class + // is implemented. + // When the Config class will be deprived of logic and + // restructured, the compiler will notify us of this kludge, but + // remember to reevaluate the code itself considering the new + // data-flow and the possibility for optimizations as this is not + // done for temporary code. Indeed some of the code is visibly wasteful. + // Similarly, ensure that the loose definition that we use here is + // not preserved. + + QStringList search_directories{config.getCanonicalPathList(CONFIG_EXAMPLEDIRS)}; + QStringList image_search_directories{config.getCanonicalPathList(CONFIG_IMAGEDIRS)}; + + const auto& [excludedDirs, excludedFiles] = config.getExcludedPaths(); + + qCDebug(lcQdoc, "Adding doc/image dirs found in exampledirs to imagedirs"); + QSet<QString> exampleImageDirs; + QStringList exampleImageList = config.getExampleImageFiles(excludedDirs, excludedFiles); + for (const auto &image : exampleImageList) { + if (image.contains("doc/images")) { + QString t = image.left(image.lastIndexOf("doc/images") + 10); + if (!exampleImageDirs.contains(t)) + exampleImageDirs.insert(t); + } + } + + // REMARK: The previous system discerned between search directories based on the kind of file that was searched for. + // For example, an image search was bounded to some directories + // that may or may not be the same as the ones where examples are + // searched for. + // The current Qt documentation does not use this feature. That + // is, the output of QDoc when a unified search list is used is + // the same as the output for that of separated lists. + // For this reason, we currently simplify the process, albeit this + // may at some point change, by joining the various lists into a + // single search list and a unified interface. + // Do note that the configuration still allows for those + // parameters to be user defined in a split-way as this will not + // be able to be changed until Config itself is looked upon. + // Hence, we join the various directory sources into one list for the time being. + // Do note that this means that the amount of searched directories for a file is now increased. + // This shouldn't matter as the amount of directories is expected + // to be generally small and the search routine complexity is + // linear in the amount of directories. + // There are some complications that may arise in very specific + // cases by this choice (some of which where there before under + // possibly different circumstances), making some files + // unreachable. + // See the remarks in FileResolver for more infomration. + std::copy(image_search_directories.begin(), image_search_directories.end(), std::back_inserter(search_directories)); + std::copy(exampleImageDirs.begin(), exampleImageDirs.end(), std::back_inserter(search_directories)); + + std::vector<DirectoryPath> validated_search_directories{}; + for (const QString& path : search_directories) { + auto maybe_validated_path{DirectoryPath::refine(path)}; + if (!maybe_validated_path) + // TODO: [uncentralized-admonition] + qCDebug(lcQdoc).noquote() << u"%1 is not a valid path, it will be ignored when resolving a file"_s.arg(path); + else validated_search_directories.push_back(*maybe_validated_path); + } + + // TODO>> + + FileResolver file_resolver{std::move(validated_search_directories)}; + + // REMARK: The constructor for generators doesn't actually perform + // initialization of their content. + // Indeed, Generators use the general antipattern of the static + // initialize-terminate non-scoped mutable state that we see in + // many parts of QDoc. + // In their constructor, Generators mainly register themselves into a static list. + // Previously, this was done at the start of main. + // To be able to pass a correct FileResolver or other systems, we + // need to construct them after the configuration has been read + // and has been destructured. + // For this reason, their construction was moved here. + // This function may be called more than once for some of QDoc's + // call, albeit this should not actually happen in Qt's + // documentation. + // Then, constructing the generators here might provide for some + // unexpected behavior as new generators are appended to the list + // and never used, considering that the list is searched in a + // linearly fashion and each generator of some type T, in the + // current codebase, will always be found if another instance of + // that same type would have been found. + // Furthermore, those instances would be destroyed by then, such + // that accessing them would be erroneous. + // To avoid this, the static list was made to be cleared in + // Generator::terminate, which, in theory, will be called before + // the generators will be constructed again. + // We could have used the initialize method for this, but this + // would force us into a limited and more complex semantic, see an + // example of this in DocParser, and would restrain us further to + // the initialize-terminate idiom which is expect to be purged in + // the future. + HtmlGenerator htmlGenerator{file_resolver}; + WebXMLGenerator webXMLGenerator{file_resolver}; + DocBookGenerator docBookGenerator{file_resolver}; + + Generator::initialize(); + + /* + Initialize the qdoc database, where all the parsed source files + will be stored. The database includes a tree of nodes, which gets + built as the source files are parsed. The documentation output is + generated by traversing that tree. + + Note: qdocDB() allocates a new instance only if no instance exists. + So it is safe to call qdocDB() any time. + */ + QDocDatabase *qdb = QDocDatabase::qdocDB(); + qdb->setVersion(config.get(CONFIG_VERSION).asString()); + /* + By default, the only output format is HTML. + */ + const QSet<QString> outputFormats = config.getOutputFormats(); + + qdb->clearSearchOrder(); + if (!config.singleExec()) { + if (!config.preparing()) { + qCDebug(lcQdoc, " loading index files"); + loadIndexFiles(outputFormats); + qCDebug(lcQdoc, " done loading index files"); + } + qdb->newPrimaryTree(project); + } else if (config.preparing()) + qdb->newPrimaryTree(project); + else + qdb->setPrimaryTree(project); + + // Retrieve the dependencies if loadIndexFiles() was not called + if (config.dependModules().isEmpty()) { + config.dependModules() = config.get(CONFIG_DEPENDS).asStringList(); + config.dependModules().removeDuplicates(); + } + qdb->setSearchOrder(config.dependModules()); + + // Store the title of the index (landing) page + NamespaceNode *root = qdb->primaryTreeRoot(); + if (root) { + QString title{config.get(CONFIG_NAVIGATION + Config::dot + CONFIG_LANDINGPAGE).asString()}; + root->tree()->setIndexTitle( + config.get(CONFIG_NAVIGATION + Config::dot + CONFIG_LANDINGTITLE).asString(title)); + } + + + std::vector<QByteArray> include_paths{}; + { + auto args = config.getCanonicalPathList(CONFIG_INCLUDEPATHS, + Config::IncludePaths); +#ifdef Q_OS_MACOS + args.append(Utilities::getInternalIncludePaths(QStringLiteral("clang++"))); +#elif defined(Q_OS_LINUX) + args.append(Utilities::getInternalIncludePaths(QStringLiteral("g++"))); +#endif + + for (const auto &path : std::as_const(args)) { + if (!path.isEmpty()) + include_paths.push_back(path.toUtf8()); + } + + include_paths.erase(std::unique(include_paths.begin(), include_paths.end()), + include_paths.end()); + } + + QList<QByteArray> clang_defines{}; + { + const QStringList config_defines{config.get(CONFIG_DEFINES).asStringList()}; + for (const QString &def : config_defines) { + if (!def.contains(QChar('*'))) { + QByteArray tmp("-D"); + tmp.append(def.toUtf8()); + clang_defines.append(tmp.constData()); + } + } + } + + std::optional<PCHFile> pch = std::nullopt; + if (config.dualExec() || config.preparing()) { + const QString moduleHeader = config.get(CONFIG_MODULEHEADER).asString(); + pch = buildPCH( + QDocDatabase::qdocDB(), + moduleHeader.isNull() ? project : moduleHeader, + Config::instance().getHeaderFiles(), + include_paths, + clang_defines + ); + } + + ClangCodeParser clangParser(QDocDatabase::qdocDB(), Config::instance(), include_paths, clang_defines, pch); + PureDocParser docParser{config.location()}; + + /* + Initialize all the classes and data structures with the + qdoc configuration. This is safe to do for each qdocconf + file processed, because all the data structures created + are either cleared after they have been used, or they + are cleared in the terminate() functions below. + */ + Location::initialize(); + Tokenizer::initialize(); + CodeMarker::initialize(); + CodeParser::initialize(); + Doc::initialize(file_resolver); + + if (config.dualExec() || config.preparing()) { + QStringList sourceList; + + qCDebug(lcQdoc, "Reading sourcedirs"); + sourceList = + config.getAllFiles(CONFIG_SOURCES, CONFIG_SOURCEDIRS, excludedDirs, excludedFiles); + + std::vector<QString> sources{}; + for (const auto &source : sourceList) { + if (source.contains(QLatin1String("doc/snippets"))) + continue; + sources.emplace_back(source); + } + /* + Find all the qdoc files in the example dirs, and add + them to the source files to be parsed. + */ + qCDebug(lcQdoc, "Reading exampledirs"); + QStringList exampleQdocList = config.getExampleQdocFiles(excludedDirs, excludedFiles); + for (const auto &example : exampleQdocList) { + sources.emplace_back(example); + } + + /* + Parse each source text file in the set using the appropriate parser and + add it to the big tree. + */ + if (config.get(CONFIG_LOGPROGRESS).asBool()) + qCInfo(lcQdoc) << "Parse source files for" << project; + + + auto headers = config.getHeaderFiles(); + CppCodeParser cpp_code_parser(FnCommandParser(qdb, headers, clang_defines, pch)); + + SourceFileParser source_file_parser{clangParser, docParser}; + parseSourceFiles(std::move(sources), source_file_parser, cpp_code_parser); + + if (config.get(CONFIG_LOGPROGRESS).asBool()) + qCInfo(lcQdoc) << "Source files parsed for" << project; + } + /* + Now the primary tree has been built from all the header and + source files. Resolve all the class names, function names, + targets, URLs, links, and other stuff that needs resolving. + */ + qCDebug(lcQdoc, "Resolving stuff prior to generating docs"); + qdb->resolveStuff(); + + /* + The primary tree is built and all the stuff that needed + resolving has been resolved. Now traverse the tree and + generate the documentation output. More than one output + format can be requested. The tree is traversed for each + one. + */ + qCDebug(lcQdoc, "Generating docs"); + for (const auto &format : outputFormats) { + auto *generator = Generator::generatorForFormat(format); + if (generator) { + generator->initializeFormat(); + generator->generateDocs(); + } else { + config.get(CONFIG_OUTPUTFORMATS) + .location() + .fatal(QStringLiteral("QDoc: Unknown output format '%1'").arg(format)); + } + } + + qCDebug(lcQdoc, "Terminating qdoc classes"); + if (Utilities::debugging()) + Utilities::stopDebugging(project); + + logStartEndMessage(QLatin1String("End"), config); + QDocDatabase::qdocDB()->setVersion(QString()); + Generator::terminate(); + CodeParser::terminate(); + CodeMarker::terminate(); + Doc::terminate(); + Tokenizer::terminate(); + Location::terminate(); + QDir::setCurrent(config.previousCurrentDir()); + + qCDebug(lcQdoc, "qdoc classes terminated"); +} + +/*! + \internal + For each file in \a qdocFiles, first clear the configured module + dependencies and then pass the file to processQdocconfFile(). + + \sa processQdocconfFile(), singleExecutionMode(), dualExecutionMode() +*/ +static void clearModuleDependenciesAndProcessQdocconfFile(const QStringList &qdocFiles) +{ + for (const auto &file : std::as_const(qdocFiles)) { + Config::instance().dependModules().clear(); + processQdocconfFile(file); + } +} + +/*! + \internal + + A single QDoc process for prepare and generate phases. + The purpose is to first generate all index files for all documentation + projects that combined make out the documentation set being generated. + This allows QDoc to link to all content contained in all projects, e.g. + user-defined types or overview documentation, regardless of the project + that content belongs to when generating the final output. +*/ +static void singleExecutionMode() +{ + const QStringList qdocFiles = Config::loadMaster(Config::instance().qdocFiles().at(0)); + + Config::instance().setQDocPass(Config::Prepare); + clearModuleDependenciesAndProcessQdocconfFile(qdocFiles); + + Config::instance().setQDocPass(Config::Generate); + QDocDatabase::qdocDB()->processForest(); + clearModuleDependenciesAndProcessQdocconfFile(qdocFiles); +} + +/*! + \internal + + Process each .qdocconf-file passed as command line argument(s). +*/ +static void dualExecutionMode() +{ + const QStringList qdocFiles = Config::instance().qdocFiles(); + clearModuleDependenciesAndProcessQdocconfFile(qdocFiles); +} + +QT_END_NAMESPACE + +int main(int argc, char **argv) +{ + QT_USE_NAMESPACE + + // Initialize Qt: +#ifndef QT_BOOTSTRAPPED + // use deterministic hash seed + QHashSeed::setDeterministicGlobalSeed(); +#endif + QCoreApplication app(argc, argv); + app.setApplicationVersion(QLatin1String(QT_VERSION_STR)); + + // Instantiate various singletons (used via static methods): + /* + Create code parsers for the languages to be parsed, + and create a tree for C++. + */ + QmlCodeParser qmlParser; + + /* + Create code markers for plain text, C++, + and QML. + + The plain CodeMarker must be instantiated first because it is used as + fallback when the other markers cannot be used. + + Each marker instance is prepended to the CodeMarker::markers list by the + base class constructor. + */ + CodeMarker fallbackMarker; + CppCodeMarker cppMarker; + QmlCodeMarker qmlMarker; + + Config::instance().init("QDoc", app.arguments()); + + if (Config::instance().qdocFiles().isEmpty()) + Config::instance().showHelp(); + + if (Config::instance().singleExec()) { + singleExecutionMode(); + } else { + dualExecutionMode(); + } + + // Tidy everything away: + QmlTypeNode::terminate(); + QDocDatabase::destroyQdocDB(); + return Location::exitCode(); +} diff --git a/src/qdoc/qdoc/src/qdoc/manifestwriter.cpp b/src/qdoc/qdoc/src/qdoc/manifestwriter.cpp new file mode 100644 index 000000000..97bf7f190 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/manifestwriter.cpp @@ -0,0 +1,405 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +#include "manifestwriter.h" + +#include "config.h" +#include "examplenode.h" +#include "generator.h" +#include "qdocdatabase.h" + +#include <QtCore/qmap.h> +#include <QtCore/qset.h> +#include <QtCore/qxmlstream.h> + +QT_BEGIN_NAMESPACE + +/*! + \internal + + For each attribute in a map of attributes, checks if the attribute is + found in \a usedAttributes. If it is not found, issues a warning specific + to the attribute. + */ +void warnAboutUnusedAttributes(const QStringList &usedAttributes, const ExampleNode *example) +{ + QMap<QString, QString> attributesToWarnFor; + attributesToWarnFor.insert(QStringLiteral("imageUrl"), + QStringLiteral("Example documentation should have at least one '\\image'")); + attributesToWarnFor.insert(QStringLiteral("projectPath"), + QStringLiteral("Example has no project file")); + + for (auto it = attributesToWarnFor.cbegin(); it != attributesToWarnFor.cend(); ++it) { + if (!usedAttributes.contains(it.key())) + example->doc().location().warning(example->name() + ": " + it.value()); + } +} + +/*! + \internal + + Write the description element. The description for an example is set + with the \brief command. If no brief is available, the element is set + to "No description available". + */ + +void writeDescription(QXmlStreamWriter *writer, const ExampleNode *example) +{ + Q_ASSERT(writer && example); + writer->writeStartElement("description"); + const Text brief = example->doc().briefText(); + if (!brief.isEmpty()) + writer->writeCDATA(brief.toString()); + else + writer->writeCDATA(QString("No description available")); + writer->writeEndElement(); // description +} + +/*! + \internal + + Returns a list of \a files that Qt Creator should open for the \a exampleName. + */ +QMap<int, QString> getFilesToOpen(const QStringList &files, const QString &exampleName) +{ + QMap<int, QString> filesToOpen; + for (const QString &file : files) { + QFileInfo fileInfo(file); + QString fileName = fileInfo.fileName().toLower(); + // open .qml, .cpp and .h files with a + // basename matching the example (project) name + // QMap key indicates the priority - + // the lowest value will be the top-most file + if ((fileInfo.baseName().compare(exampleName, Qt::CaseInsensitive) == 0)) { + if (fileName.endsWith(".qml")) + filesToOpen.insert(0, file); + else if (fileName.endsWith(".cpp")) + filesToOpen.insert(1, file); + else if (fileName.endsWith(".h")) + filesToOpen.insert(2, file); + } + // main.qml takes precedence over main.cpp + else if (fileName.endsWith("main.qml")) { + filesToOpen.insert(3, file); + } else if (fileName.endsWith("main.cpp")) { + filesToOpen.insert(4, file); + } + } + + return filesToOpen; +} + +/*! + \internal + \brief Writes the lists of files to open for the example. + + Writes out the \a filesToOpen and the full \a installPath through \a writer. + */ +void writeFilesToOpen(QXmlStreamWriter &writer, const QString &installPath, + const QMap<int, QString> &filesToOpen) +{ + for (auto it = filesToOpen.constEnd(); it != filesToOpen.constBegin();) { + writer.writeStartElement("fileToOpen"); + if (--it == filesToOpen.constBegin()) { + writer.writeAttribute(QStringLiteral("mainFile"), QStringLiteral("true")); + } + writer.writeCharacters(installPath + it.value()); + writer.writeEndElement(); + } +} + +/*! + \internal + \brief Writes example metadata into \a writer. + + For instance, + + + \ meta category {Application Example} + + becomes + + <meta> + <entry name="category">Application Example</entry> + <meta> +*/ +static void writeMetaInformation(QXmlStreamWriter &writer, const QStringMultiMap &map) +{ + if (map.isEmpty()) + return; + + writer.writeStartElement("meta"); + for (auto it = map.constBegin(); it != map.constEnd(); ++it) { + writer.writeStartElement("entry"); + writer.writeAttribute(QStringLiteral("name"), it.key()); + writer.writeCharacters(it.value()); + writer.writeEndElement(); // tag + } + writer.writeEndElement(); // meta +} + +/*! + \class ManifestWriter + \internal + \brief The ManifestWriter is responsible for writing manifest files. + */ +ManifestWriter::ManifestWriter() +{ + Config &config = Config::instance(); + m_project = config.get(CONFIG_PROJECT).asString(); + m_outputDirectory = config.getOutputDir(); + m_qdb = QDocDatabase::qdocDB(); + + const QString prefix = CONFIG_QHP + Config::dot + m_project + Config::dot; + m_manifestDir = + QLatin1String("qthelp://") + config.get(prefix + QLatin1String("namespace")).asString(); + m_manifestDir += + QLatin1Char('/') + config.get(prefix + QLatin1String("virtualFolder")).asString() + + QLatin1Char('/'); + readManifestMetaContent(); + m_examplesPath = config.get(CONFIG_EXAMPLESINSTALLPATH).asString(); + if (!m_examplesPath.isEmpty()) + m_examplesPath += QLatin1Char('/'); +} + +template <typename F> +void ManifestWriter::processManifestMetaContent(const QString &fullName, F matchFunc) +{ + for (const auto &index : m_manifestMetaContent) { + const auto &names = index.m_names; + for (const QString &name : names) { + bool match; + qsizetype wildcard = name.indexOf(QChar('*')); + switch (wildcard) { + case -1: // no wildcard used, exact match required + match = (fullName == name); + break; + case 0: // '*' matches all examples + match = true; + break; + default: // match with wildcard at the end + match = fullName.startsWith(name.left(wildcard)); + } + if (match) + matchFunc(index); + } + } +} + +/*! + This function outputs one or more manifest files in XML. + They are used by Creator. + */ +void ManifestWriter::generateManifestFiles() +{ + generateExampleManifestFile(); + m_qdb->exampleNodeMap().clear(); + m_manifestMetaContent.clear(); +} + +/* + Returns Qt module name as lower case tag, stripping Qt prefix: + QtQuickControls -> quickcontrols + QtOpenGL -> opengl + QtQuick3D -> quick3d + */ +static QString moduleNameAsTag(const QString &module) +{ + QString moduleName = module; + if (moduleName.startsWith("Qt")) + moduleName = moduleName.mid(2); + // Some examples are in QtDoc module, but 'doc' as tag makes little sense + if (moduleName == "Doc") + return QString(); + return moduleName.toLower(); +} + +/* + Return tags that were added with + \ meta {tag} {tag1[,tag2,...]} + or + \ meta {tags} {tag1[,tag2,...]} + from example metadata + */ +static QSet<QString> tagsAddedWithMetaCommand(const ExampleNode *example) +{ + Q_ASSERT(example); + + QSet<QString> tags; + const QStringMultiMap *metaTagMap = example->doc().metaTagMap(); + if (metaTagMap) { + QStringList originalTags = metaTagMap->values("tag"); + originalTags << metaTagMap->values("tags"); + for (const auto &tag : originalTags) { + const auto &tagList = tag.toLower().split(QLatin1Char(','), Qt::SkipEmptyParts); + tags += QSet<QString>(tagList.constBegin(), tagList.constEnd()); + } + } + return tags; +} + +/* + Writes the contents of tags into writer, formatted as + <tags>tag1,tag2..</tags> + */ +static void writeTagsElement(QXmlStreamWriter *writer, const QSet<QString> &tags) +{ + Q_ASSERT(writer); + if (tags.isEmpty()) + return; + + writer->writeStartElement("tags"); + QStringList sortedTags = tags.values(); + sortedTags.sort(); + writer->writeCharacters(sortedTags.join(",")); + writer->writeEndElement(); // tags +} + +/*! + This function is called by generateExampleManifestFiles(), once + for each manifest file to be generated. + */ +void ManifestWriter::generateExampleManifestFile() +{ + const ExampleNodeMap &exampleNodeMap = m_qdb->exampleNodeMap(); + if (exampleNodeMap.isEmpty()) + return; + + const QString outputFileName = "examples-manifest.xml"; + QFile outputFile(m_outputDirectory + QLatin1Char('/') + outputFileName); + if (!outputFile.open(QFile::WriteOnly | QFile::Text)) + return; + + QXmlStreamWriter writer(&outputFile); + writer.setAutoFormatting(true); + writer.writeStartDocument(); + writer.writeStartElement("instructionals"); + writer.writeAttribute("module", m_project); + writer.writeStartElement("examples"); + + for (const auto &example : exampleNodeMap.values()) { + QMap<QString, QString> usedAttributes; + QSet<QString> tags; + const QString installPath = retrieveExampleInstallationPath(example); + const QString fullName = m_project + QLatin1Char('/') + example->title(); + + processManifestMetaContent( + fullName, [&](const ManifestMetaFilter &filter) { tags += filter.m_tags; }); + tags += tagsAddedWithMetaCommand(example); + // omit from the manifest if explicitly marked broken + if (tags.contains("broken")) + continue; + + // attributes that are always written for the element + usedAttributes.insert("name", example->title()); + usedAttributes.insert("docUrl", m_manifestDir + Generator::currentGenerator()->fileBase(example) + ".html"); + + if (!example->projectFile().isEmpty()) + usedAttributes.insert("projectPath", installPath + example->projectFile()); + if (!example->imageFileName().isEmpty()) + usedAttributes.insert("imageUrl", m_manifestDir + example->imageFileName()); + + processManifestMetaContent(fullName, [&](const ManifestMetaFilter &filter) { + const auto attributes = filter.m_attributes; + for (const auto &attribute : attributes) { + const QLatin1Char div(':'); + QStringList attrList = attribute.split(div); + if (attrList.size() == 1) + attrList.append(QStringLiteral("true")); + QString attrName = attrList.takeFirst(); + if (!usedAttributes.contains(attrName)) + usedAttributes.insert(attrName, attrList.join(div)); + } + }); + + writer.writeStartElement("example"); + for (auto it = usedAttributes.cbegin(); it != usedAttributes.cend(); ++it) + writer.writeAttribute(it.key(), it.value()); + + warnAboutUnusedAttributes(usedAttributes.keys(), example); + writeDescription(&writer, example); + + const QString moduleNameTag = moduleNameAsTag(m_project); + if (!moduleNameTag.isEmpty()) + tags << moduleNameTag; + writeTagsElement(&writer, tags); + + const QString exampleName = example->name().mid(example->name().lastIndexOf('/') + 1); + const auto files = example->files(); + const QMap<int, QString> filesToOpen = getFilesToOpen(files, exampleName); + writeFilesToOpen(writer, installPath, filesToOpen); + + if (const QStringMultiMap *metaTagMapP = example->doc().metaTagMap()) { + // Write \meta elements into the XML, except for 'tag', 'installpath', + // as they are handled separately + QStringMultiMap map = *metaTagMapP; + erase_if(map, [](QStringMultiMap::iterator iter) { + return iter.key() == "tag" || iter.key() == "tags" || iter.key() == "installpath"; + }); + writeMetaInformation(writer, map); + } + + writer.writeEndElement(); // example + } + + writer.writeEndElement(); // examples + + if (!m_exampleCategories.isEmpty()) { + writer.writeStartElement("categories"); + for (const auto &examplecategory : m_exampleCategories) { + writer.writeStartElement("category"); + writer.writeCharacters(examplecategory); + writer.writeEndElement(); + } + writer.writeEndElement(); // categories + } + + writer.writeEndElement(); // instructionals + writer.writeEndDocument(); + outputFile.close(); +} + +/*! + Reads metacontent - additional attributes and tags to apply + when generating manifest files, read from config. + + The manifest metacontent map is cleared immediately after + the manifest files have been generated. + */ +void ManifestWriter::readManifestMetaContent() +{ + Config &config = Config::instance(); + const QStringList names{config.get(CONFIG_MANIFESTMETA + + Config::dot + + QStringLiteral("filters")).asStringList()}; + + for (const auto &manifest : names) { + ManifestMetaFilter filter; + QString prefix = CONFIG_MANIFESTMETA + Config::dot + manifest + Config::dot; + filter.m_names = config.get(prefix + QStringLiteral("names")).asStringSet(); + filter.m_attributes = config.get(prefix + QStringLiteral("attributes")).asStringSet(); + filter.m_tags = config.get(prefix + QStringLiteral("tags")).asStringSet(); + m_manifestMetaContent.append(filter); + } + + m_exampleCategories = config.get(CONFIG_MANIFESTMETA + + QStringLiteral(".examplecategories")).asStringList(); +} + +/*! + Retrieve the install path for the \a example as specified with + the \\meta command, or fall back to the one defined in .qdocconf. + */ +QString ManifestWriter::retrieveExampleInstallationPath(const ExampleNode *example) const +{ + QString installPath; + if (example->doc().metaTagMap()) + installPath = example->doc().metaTagMap()->value(QLatin1String("installpath")); + if (installPath.isEmpty()) + installPath = m_examplesPath; + if (!installPath.isEmpty() && !installPath.endsWith(QLatin1Char('/'))) + installPath += QLatin1Char('/'); + + return installPath; +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/manifestwriter.h b/src/qdoc/qdoc/src/qdoc/manifestwriter.h new file mode 100644 index 000000000..730835b9e --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/manifestwriter.h @@ -0,0 +1,46 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +#ifndef MANIFESTWRITER_H +#define MANIFESTWRITER_H + +#include <QtCore/qlist.h> +#include <QtCore/qset.h> +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +class ExampleNode; +class QDocDatabase; +class QXmlStreamWriter; +class ManifestWriter +{ + struct ManifestMetaFilter + { + QSet<QString> m_names {}; + QSet<QString> m_attributes {}; + QSet<QString> m_tags {}; + }; + +public: + ManifestWriter(); + void generateManifestFiles(); + void generateExampleManifestFile(); + void readManifestMetaContent(); + QString retrieveExampleInstallationPath(const ExampleNode *example) const; + +private: + QString m_manifestDir {}; + QString m_examplesPath {}; + QString m_outputDirectory {}; + QString m_project {}; + QDocDatabase *m_qdb { nullptr }; + QList<ManifestMetaFilter> m_manifestMetaContent {}; + QStringList m_exampleCategories {}; + + template <typename F> + void processManifestMetaContent(const QString &fullName, F matchFunc); +}; + +QT_END_NAMESPACE + +#endif // MANIFESTWRITER_H diff --git a/src/qdoc/qdoc/src/qdoc/namespacenode.cpp b/src/qdoc/qdoc/src/qdoc/namespacenode.cpp new file mode 100644 index 000000000..22686c050 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/namespacenode.cpp @@ -0,0 +1,190 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "namespacenode.h" + +#include "codeparser.h" +#include "tree.h" + +QT_BEGIN_NAMESPACE + +/*! + \class NamespaceNode + \brief This class represents a C++ namespace. + + A namespace can be used in multiple C++ modules, so there + can be a NamespaceNode for namespace Xxx in more than one + Node tree. + */ + +/*! \fn NamespaceNode(Aggregate *parent, const QString &name) + Constructs a NamespaceNode with the specified \a parent and \a name. + The node type is Node::Namespace. + */ + +/*! + Returns true if this namespace is to be documented in the + current module. There can be elements declared in this + namespace spread over multiple modules. Those elements are + documented in the modules where they are declared, but they + are linked to from the namespace page in the module where + the namespace itself is documented. + */ +bool NamespaceNode::isDocumentedHere() const +{ + return m_whereDocumented == tree()->camelCaseModuleName(); +} + +/*! + Returns true if this namespace node contains at least one + child that has documentation and is not private or internal. + */ +bool NamespaceNode::hasDocumentedChildren() const +{ + return std::any_of(m_children.cbegin(), m_children.cend(), + [](Node *child) { return child->isInAPI(); }); +} + +/*! + Report qdoc warning for each documented child in a namespace + that is not documented. This function should only be called + when the namespace is not documented. + */ +void NamespaceNode::reportDocumentedChildrenInUndocumentedNamespace() const +{ + for (const auto *node : std::as_const(m_children)) { + if (node->isInAPI()) { + QString msg1 = node->name(); + if (node->isFunction()) + msg1 += "()"; + msg1 += QStringLiteral( + " is documented, but namespace %1 is not documented in any module.") + .arg(name()); + QString msg2 = + QStringLiteral( + "Add /*! '\\%1 %2' ... */ or remove the qdoc comment marker (!) at " + "that line number.") + .arg(COMMAND_NAMESPACE, name()); + + node->doc().location().warning(msg1, msg2); + } + } +} + +/*! + Returns true if this namespace node is not private and + contains at least one public child node with documentation. + */ +bool NamespaceNode::docMustBeGenerated() const +{ + if (isInAPI()) + return true; + return hasDocumentedChildren(); +} + +/*! + Returns a const reference to the namespace node's list of + included children, which contains pointers to all the child + nodes of other namespace nodes that have the same name as + this namespace node. The list is built after the prepare + phase has been run but just before the generate phase. It + is buils by QDocDatabase::resolveNamespaces(). + + \sa QDocDatabase::resolveNamespaces() + */ +const NodeList &NamespaceNode::includedChildren() const +{ + return m_includedChildren; +} + +/*! + This function is only called from QDocDatabase::resolveNamespaces(). + + \sa includedChildren(), QDocDatabase::resolveNamespaces() + */ +void NamespaceNode::includeChild(Node *child) +{ + m_includedChildren.append(child); +} + +/*! \fn Tree* NamespaceNode::tree() const + Returns a pointer to the Tree that contains this NamespaceNode. + This requires traversing the parent() pointers to the root of + the Tree, which is the unnamed NamespaceNode. + */ + +/*! \fn bool NamespaceNode::isFirstClassAggregate() const + Returns \c true. + */ + +/*! \fn bool NamespaceNode::isRelatableType() const + Returns \c true. + */ + +/*! \fn bool NamespaceNode::wasSeen() const + Returns \c true if the \c {\\namespace} command that this NamespaceNode + represents has been parsed by qdoc. When \c false is returned, it means + that only \c {\\relates} commands have been seen that relate elements to + this namespace. + */ + +/*! \fn void NamespaceNode::markSeen() + Sets the data member that indicates that the \c {\\namespace} command this + NamespaceNode represents has been parsed by qdoc. + */ + +/*! \fn void NamespaceNode::markNotSeen() + Clears the data member that indicates that the \c {\\namespace} command this + NamespaceNode represents has been parsed by qdoc. + */ + +/*! \fn void NamespaceNode::setTree(Tree* t) + Sets the Tree pointer to \a t, which means this NamespaceNode is in the Tree \a t. + */ + +/*! \fn QString NamespaceNode::whereDocumented() const + Returns the camel case name of the module where this namespace is documented. + + \sa setWhereDocumented() + */ + +/*! \fn void NamespaceNode::setWhereDocumented(const QString &t) + Sets the camel case name of the module where this namespace is documented to + the module named \a t. + + This function is called when the \c {\\namespace} command is processed to let + qdoc know that this namespace is documented in the current module, so that + when something in another module is marked as related to this namespace, it + can be documented there with a ProxyNode for this namespace. + + \sa whereDocumented() + */ + +/*! \fn void NamespaceNode::setDocumented() + Sets the flag indicating that the \c {\\namespace} command for this + namespace was seen. + */ + +/*! \fn bool NamespaceNode::wasDocumented() const + Returns \c true if a \c {\\namespace} command for this namespace was seen. + Otherwise returns \c false. + */ + +/*! \fn void NamespaceNode::setDocNode(NamespaceNode *ns) + Called in QDocDatabase::resolveNamespaces() to set the pointer to the + NamespaceNode in which this namespace is documented. + + \sa QDocDatabase::resolveNamespaces() + */ + +/*! \fn NamespaceNode *NamespaceNode::docNode() const + Returns a pointer to the NamespaceNode that represents where the namespace + documentation is actually generated. API elements in many different modules + can be included in a single namespace. That namespace is only documented in + one module. The namespace is documented in the module where the \c {\\namespace} + command for the namespace appears. + + \sa QDocDatabase::resolveNamespaces() + */ + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/namespacenode.h b/src/qdoc/qdoc/src/qdoc/namespacenode.h new file mode 100644 index 000000000..80d068838 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/namespacenode.h @@ -0,0 +1,48 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef NAMESPACENODE_H +#define NAMESPACENODE_H + +#include "aggregate.h" + +#include <QtCore/qglobal.h> +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +class Tree; + +class NamespaceNode : public Aggregate +{ +public: + NamespaceNode(Aggregate *parent, const QString &name) : Aggregate(Namespace, parent, name) {} + ~NamespaceNode() override = default; + [[nodiscard]] Tree *tree() const override { return (parent() ? parent()->tree() : m_tree); } + + [[nodiscard]] bool isFirstClassAggregate() const override { return true; } + [[nodiscard]] bool isRelatableType() const override { return true; } + [[nodiscard]] bool wasSeen() const override { return m_seen; } + void markSeen() { m_seen = true; } + void setTree(Tree *t) { m_tree = t; } + [[nodiscard]] const NodeList &includedChildren() const; + void includeChild(Node *child); + void setWhereDocumented(const QString &t) { m_whereDocumented = t; } + [[nodiscard]] bool isDocumentedHere() const; + [[nodiscard]] bool hasDocumentedChildren() const; + void reportDocumentedChildrenInUndocumentedNamespace() const; + [[nodiscard]] bool docMustBeGenerated() const override; + void setDocNode(NamespaceNode *ns) { m_docNode = ns; } + [[nodiscard]] NamespaceNode *docNode() const { return m_docNode; } + +private: + bool m_seen { false }; + Tree *m_tree { nullptr }; + QString m_whereDocumented {}; + NamespaceNode *m_docNode { nullptr }; + NodeList m_includedChildren {}; +}; + +QT_END_NAMESPACE + +#endif // NAMESPACENODE_H diff --git a/src/qdoc/qdoc/src/qdoc/node.cpp b/src/qdoc/qdoc/src/qdoc/node.cpp new file mode 100644 index 000000000..2857aecba --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/node.cpp @@ -0,0 +1,1412 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "node.h" + +#include "aggregate.h" +#include "codemarker.h" +#include "config.h" +#include "enumnode.h" +#include "functionnode.h" +#include "generator.h" +#include "qdocdatabase.h" +#include "qmltypenode.h" +#include "qmlpropertynode.h" +#include "relatedclass.h" +#include "sharedcommentnode.h" +#include "tokenizer.h" +#include "tree.h" + +#include <QtCore/quuid.h> +#include <QtCore/qversionnumber.h> + +#include <utility> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +/*! + \class Node + \brief The Node class is the base class for all the nodes in QDoc's parse tree. + + Class Node is the base class of all the node subclasses. There is a subclass of Node + for each type of entity that QDoc can document. The types of entities that QDoc can + document are listed in the enum type NodeType. + + After ClangCodeParser has parsed all the header files to build its precompiled header, + it then visits the clang Abstract Syntax Tree (AST). For each node in the AST that it + determines is in the public API and must be documented, it creates an instance of one + of the Node subclasses and adds it to the QDoc Tree. + + Each instance of a sublass of Node has a parent pointer to link it into the Tree. The + parent pointer is obtained by calling \l {parent()}, which returns a pointer to an + instance of the Node subclass, Aggregate, which is never instantiated directly, but + as the base class for certain subclasses of Node that can have children. For example, + ClassNode and QmlTypeNode can have children, so they both inherit Aggregate, while + PropertyNode and QmlPropertyNode can not have children, so they both inherit Node. + + \sa Aggregate, ClassNode, PropertyNode + */ + +/*! + Returns \c true if the node \a n1 is less than node \a n2. The + comparison is performed by comparing properties of the nodes + in order of increasing complexity. + */ +bool Node::nodeNameLessThan(const Node *n1, const Node *n2) +{ +#define LT_RETURN_IF_NOT_EQUAL(a, b) \ + if ((a) != (b)) \ + return (a) < (b); + + if (n1->isPageNode() && n2->isPageNode()) { + LT_RETURN_IF_NOT_EQUAL(n1->fullName(), n2->fullName()); + LT_RETURN_IF_NOT_EQUAL(n1->fullTitle(), n2->fullTitle()); + } + + if (n1->isFunction() && n2->isFunction()) { + const auto *f1 = static_cast<const FunctionNode *>(n1); + const auto *f2 = static_cast<const FunctionNode *>(n2); + + LT_RETURN_IF_NOT_EQUAL(f1->isConst(), f2->isConst()); + LT_RETURN_IF_NOT_EQUAL(f1->signature(Node::SignatureReturnType), + f2->signature(Node::SignatureReturnType)); + } + + LT_RETURN_IF_NOT_EQUAL(n1->nodeType(), n2->nodeType()); + LT_RETURN_IF_NOT_EQUAL(n1->name(), n2->name()); + LT_RETURN_IF_NOT_EQUAL(n1->access(), n2->access()); + LT_RETURN_IF_NOT_EQUAL(n1->location().filePath(), n2->location().filePath()); + + return false; +} + + +/*! + Returns \c true if node \a n1 is less than node \a n2 when comparing + the sort keys, defined with + + \badcode + \meta sortkey {<value>} + \endcode + + in the respective nodes' documentation. If the two sort keys are equal, + falls back to nodeNameLessThan(). If \a n1 defines a sort key and \a n2 + does not, then n1 < n2. + +*/ +bool Node::nodeSortKeyOrNameLessThan(const Node *n1, const Node *n2) +{ + const QString default_sortkey{QChar{QChar::LastValidCodePoint}}; + const auto *n1_metamap{n1->doc().metaTagMap()}; + const auto *n2_metamap{n2->doc().metaTagMap()}; + if (auto cmp = QString::compare( + n1_metamap ? n1_metamap->value(u"sortkey"_s, default_sortkey) : default_sortkey, + n2_metamap ? n2_metamap->value(u"sortkey"_s, default_sortkey) : default_sortkey); cmp != 0) { + return cmp < 0; + } + return nodeNameLessThan(n1, n2); +} + +/*! + \enum Node::NodeType + + An unsigned char value that identifies an object as a + particular subclass of Node. + + \value NoType The node type has not been set yet. + \value Namespace The Node subclass is NamespaceNode, which represents a C++ + namespace. + \value Class The Node subclass is ClassNode, which represents a C++ class. + \value Struct The Node subclass is ClassNode, which represents a C struct. + \value Union The Node subclass is ClassNode, which represents a C union + (currently no known uses). + \value HeaderFile The Node subclass is HeaderNode, which represents a header + file. + \value Page The Node subclass is PageNode, which represents a text page from + a .qdoc file. + \value Enum The Node subclass is EnumNode, which represents an enum type or + enum class. + \value Example The Node subclass is ExampleNode, which represents an example + subdirectory. + \value ExternalPage The Node subclass is ExternalPageNode, which is for + linking to an external page. + \value Function The Node subclass is FunctionNode, which can represent C++, + and QML functions. + \value Typedef The Node subclass is TypedefNode, which represents a C++ + typedef. + \value Property The Node subclass is PropertyNode, which represents a use of + Q_Property. + \value Variable The Node subclass is VariableNode, which represents a global + variable or class data member. + \value Group The Node subclass is CollectionNode, which represents a group of + documents. + \value Module The Node subclass is CollectionNode, which represents a C++ + module. + \value QmlType The Node subclass is QmlTypeNode, which represents a QML type. + \value QmlModule The Node subclass is CollectionNode, which represents a QML + module. + \value QmlProperty The Node subclass is QmlPropertyNode, which represents a + property in a QML type. + \value QmlBasicType The Node subclass is QmlTypeNode, which represents a + value type like int, etc. + \value SharedComment The Node subclass is SharedCommentNode, which represents + a collection of nodes that share the same documentation comment. + \omitvalue Collection + \value Proxy The Node subclass is ProxyNode, which represents one or more + entities that are documented in the current module but which actually + reside in a different module. + \omitvalue LastType +*/ + +/*! + \enum Node::Genus + + An unsigned char value that specifies whether the Node represents a + C++ element, a QML element, or a text document. + The Genus values are also passed to search functions to specify the + Genus of Tree Node that can satisfy the search. + + \value DontCare The Genus is not specified. Used when calling Tree search functions to indicate + the search can accept any Genus of Node. + \value CPP The Node represents a C++ element. + \value QML The Node represents a QML element. + \value DOC The Node represents a text document. +*/ + +/*! + \internal + \fn setComparisonCategory(const ComparisonCategory category) + + Sets the comparison category of this node to \a category. + + \sa ComparisonCategory, comparisonCategory() + */ + +/*! + \internal + \fn ComparisonCategory comparisonCategory() + + Returns the comparison category of this node. + + \sa ComparisonCategory, setComparisonCategory() + */ + +/*! + \enum Access + + An unsigned char value that indicates the C++ access level. + + \value Public The element has public access. + \value Protected The element has protected access. + \value Private The element has private access. +*/ + +/*! + \enum Node::Status + + An unsigned char that specifies the status of the documentation element in + the documentation set. + + \value Deprecated The element has been deprecated. + \value Preliminary The element is new; the documentation is preliminary. + \value Active The element is current. + \value Internal The element is for internal use only, not to be published. + \value DontDocument The element is not to be documented. +*/ + +/*! + \enum Node::ThreadSafeness + + An unsigned char that specifies the degree of thread-safeness of the element. + + \value UnspecifiedSafeness The thread-safeness is not specified. + \value NonReentrant The element is not reentrant. + \value Reentrant The element is reentrant. + \value ThreadSafe The element is threadsafe. +*/ + +/*! + \enum Node::LinkType + + An unsigned char value that probably should be moved out of the Node base class. + + \value StartLink + \value NextLink + \value PreviousLink + \value ContentsLink + */ + +/*! + \enum Node::FlagValue + + A value used in PropertyNode and QmlPropertyNode that can be -1, 0, or +1. + Properties and QML properties have flags, which can be 0 or 1, false or true, + or not set. FlagValueDefault is the not set value. In other words, if a flag + is set to FlagValueDefault, the meaning is the flag has not been set. + + \value FlagValueDefault -1 Not set. + \value FlagValueFalse 0 False. + \value FlagValueTrue 1 True. +*/ + +/*! + \fn Node::~Node() + + The default destructor is virtual so any subclass of Node can be + deleted by deleting a pointer to Node. + */ + +/*! \fn bool Node::isActive() const + Returns true if this node's status is \c Active. + */ + +/*! \fn bool Node::isClass() const + Returns true if the node type is \c Class. + */ + +/*! \fn bool Node::isCppNode() const + Returns true if this node's Genus value is \c CPP. + */ + +/*! \fn bool Node::isDeprecated() const + Returns true if this node's status is \c Deprecated. + */ + +/*! \fn bool Node::isDontDocument() const + Returns true if this node's status is \c DontDocument. + */ + +/*! \fn bool Node::isEnumType() const + Returns true if the node type is \c Enum. + */ + +/*! \fn bool Node::isExample() const + Returns true if the node type is \c Example. + */ + +/*! \fn bool Node::isExternalPage() const + Returns true if the node type is \c ExternalPage. + */ + +/*! \fn bool Node::isFunction(Genus g = DontCare) const + Returns true if this is a FunctionNode and its Genus is set to \a g. + */ + +/*! \fn bool Node::isGroup() const + Returns true if the node type is \c Group. + */ + +/*! \fn bool Node::isHeader() const + Returns true if the node type is \c HeaderFile. + */ + +/*! \fn bool Node::isIndexNode() const + Returns true if this node was created from something in an index file. + */ + +/*! \fn bool Node::isModule() const + Returns true if the node type is \c Module. + */ + +/*! \fn bool Node::isNamespace() const + Returns true if the node type is \c Namespace. + */ + +/*! \fn bool Node::isPage() const + Returns true if the node type is \c Page. + */ + +/*! \fn bool Node::isPreliminary() const + Returns true if this node's status is \c Preliminary. + */ + +/*! \fn bool Node::isPrivate() const + Returns true if this node's access is \c Private. + */ + +/*! \fn bool Node::isProperty() const + Returns true if the node type is \c Property. + */ + +/*! \fn bool Node::isProxyNode() const + Returns true if the node type is \c Proxy. + */ + +/*! \fn bool Node::isPublic() const + Returns true if this node's access is \c Public. + */ + +/*! \fn bool Node::isProtected() const + Returns true if this node's access is \c Protected. + */ + +/*! \fn bool Node::isQmlBasicType() const + Returns true if the node type is \c QmlBasicType. + */ + +/*! \fn bool Node::isQmlModule() const + Returns true if the node type is \c QmlModule. + */ + +/*! \fn bool Node::isQmlNode() const + Returns true if this node's Genus value is \c QML. + */ + +/*! \fn bool Node::isQmlProperty() const + Returns true if the node type is \c QmlProperty. + */ + +/*! \fn bool Node::isQmlType() const + Returns true if the node type is \c QmlType or \c QmlValueType. + */ + +/*! \fn bool Node::isRelatedNonmember() const + Returns true if this is a related nonmember of something. + */ + +/*! \fn bool Node::isStruct() const + Returns true if the node type is \c Struct. + */ + +/*! \fn bool Node::isSharedCommentNode() const + Returns true if the node type is \c SharedComment. + */ + +/*! \fn bool Node::isTypeAlias() const + Returns true if the node type is \c Typedef. + */ + +/*! \fn bool Node::isTypedef() const + Returns true if the node type is \c Typedef. + */ + +/*! \fn bool Node::isUnion() const + Returns true if the node type is \c Union. + */ + +/*! \fn bool Node::isVariable() const + Returns true if the node type is \c Variable. + */ + +/*! \fn bool Node::isGenericCollection() const + Returns true if the node type is \c Collection. + */ + +/*! \fn bool Node::isAbstract() const + Returns true if the ClassNode or QmlTypeNode is marked abstract. +*/ + +/*! \fn bool Node::isAggregate() const + Returns true if this node is an aggregate, which means it + inherits Aggregate and can therefore have children. +*/ + +/*! \fn bool Node::isFirstClassAggregate() const + Returns true if this Node is an Aggregate but not a ProxyNode. +*/ + +/*! \fn bool Node::isAlias() const + Returns true if this QML property is marked as an alias. +*/ + +/*! \fn bool Node::isAttached() const + Returns true if the QML property or QML method node is marked as attached. +*/ + +/*! \fn bool Node::isClassNode() const + Returns true if this is an instance of ClassNode. +*/ + +/*! \fn bool Node::isCollectionNode() const + Returns true if this is an instance of CollectionNode. +*/ + +/*! \fn bool Node::isDefault() const + Returns true if the QML property node is marked as default. +*/ + +/*! \fn bool Node::isMacro() const + returns true if either FunctionNode::isMacroWithParams() or + FunctionNode::isMacroWithoutParams() returns true. +*/ + +/*! \fn bool Node::isPageNode() const + Returns true if this node represents something that generates a documentation + page. In other words, if this Node's subclass inherits PageNode, then this + function will return \e true. +*/ + +/*! \fn bool Node::isRelatableType() const + Returns true if this node is something you can relate things to with + the \e relates command. NamespaceNode, ClassNode, HeaderNode, and + ProxyNode are relatable types. +*/ + +/*! \fn bool Node::isMarkedReimp() const + Returns true if the FunctionNode is marked as a reimplemented function. + That means it is virtual in the base class and it is reimplemented in + the subclass. +*/ + +/*! \fn bool Node::isPropertyGroup() const + Returns true if the node is a SharedCommentNode for documenting + multiple C++ properties or multiple QML properties. +*/ + +/*! \fn bool Node::isStatic() const + Returns true if the FunctionNode represents a static function. +*/ + +/*! \fn bool Node::isTextPageNode() const + Returns true if the node is a PageNode but not an Aggregate. +*/ + +/*! + Returns this node's name member. Appends "()" to the returned + name if this node is a function node, but not if it is a macro + because macro names normally appear without parentheses. + */ +QString Node::plainName() const +{ + if (isFunction() && !isMacro()) + return m_name + QLatin1String("()"); + return m_name; +} + +/*! + Constructs and returns the node's fully qualified name by + recursively ascending the parent links and prepending each + parent name + "::". Breaks out when reaching a HeaderNode, + or when the parent pointer is \a relative. Typically, calls + to this function pass \c nullptr for \a relative. + */ +QString Node::plainFullName(const Node *relative) const +{ + if (m_name.isEmpty()) + return QLatin1String("global"); + if (isHeader()) + return plainName(); + + QStringList parts; + const Node *node = this; + while (node && !node->isHeader()) { + parts.prepend(node->plainName()); + if (node->parent() == relative || node->parent()->name().isEmpty()) + break; + node = node->parent(); + } + return parts.join(QLatin1String("::")); +} + +/*! + Constructs and returns the node's fully qualified signature + by recursively ascending the parent links and prepending each + parent name + "::" to the plain signature. The return type is + not included. + */ +QString Node::plainSignature() const +{ + if (m_name.isEmpty()) + return QLatin1String("global"); + + QString fullName; + const Node *node = this; + while (node) { + fullName.prepend(node->signature(Node::SignaturePlain)); + if (node->parent()->name().isEmpty()) + break; + fullName.prepend(QLatin1String("::")); + node = node->parent(); + } + return fullName; +} + +/*! + Constructs and returns this node's full name. The full name is + often just the title(). When it is not the title, it is the + plainFullName(). + */ +QString Node::fullName(const Node *relative) const +{ + if ((isTextPageNode() || isGroup()) && !title().isEmpty()) + return title(); + return plainFullName(relative); +} + +/*! + Sets this Node's Doc to \a doc. If \a replace is false and + this Node already has a Doc, and if this doc is not marked + with the \\reimp command, a warning is reported that the + existing Doc is being overridden, and it reports where the + previous Doc was found. If \a replace is true, the Doc is + replaced silently. + */ +void Node::setDoc(const Doc &doc, bool replace) +{ + if (!m_doc.isEmpty() && !replace && !doc.isMarkedReimp()) { + doc.location().warning(QStringLiteral("Overrides a previous doc"), + QStringLiteral("from here: %1").arg(m_doc.location().toString())); + } + m_doc = doc; +} + +/*! + Sets the node's status to \a t. + + \sa Status +*/ +void Node::setStatus(Status t) +{ + m_status = t; + + // Set non-null, empty URL to nodes that are ignored as + // link targets + switch (t) { + case Internal: + if (Config::instance().showInternal()) + break; + Q_FALLTHROUGH(); + case DontDocument: + m_url = QStringLiteral(""); + break; + default: + break; + } +} + +/*! + Construct a node with the given \a type and having the + given \a parent and \a name. The new node is added to the + parent's child list. + */ +Node::Node(NodeType type, Aggregate *parent, QString name) + : m_nodeType(type), + m_indexNodeFlag(false), + m_relatedNonmember(false), + m_hadDoc(false), + m_parent(parent), + m_name(std::move(name)) +{ + if (m_parent) + m_parent->addChild(this); + + setGenus(getGenus(type)); +} + +/*! + Determines the appropriate Genus value for the NodeType + value \a t and returns that Genus value. Note that this + function is called in the Node() constructor. It always + returns Node::CPP when \a t is Node::Function, which + means the FunctionNode() constructor must determine its + own Genus value separately, because class FunctionNode + is a subclass of Node. + */ +Node::Genus Node::getGenus(Node::NodeType t) +{ + switch (t) { + case Node::Enum: + case Node::Class: + case Node::Struct: + case Node::Union: + case Node::Module: + case Node::TypeAlias: + case Node::Typedef: + case Node::Property: + case Node::Variable: + case Node::Function: + case Node::Namespace: + case Node::HeaderFile: + return Node::CPP; + case Node::QmlType: + case Node::QmlModule: + case Node::QmlProperty: + case Node::QmlValueType: + return Node::QML; + case Node::Page: + case Node::Group: + case Node::Example: + case Node::ExternalPage: + return Node::DOC; + case Node::Collection: + case Node::SharedComment: + case Node::Proxy: + default: + return Node::DontCare; + } +} + +/*! \fn QString Node::url() const + Returns the node's URL, which is the url of the documentation page + created for the node or the url of an external page if the node is + an ExternalPageNode. The url is used for generating a link to the + page the node represents. + + \sa Node::setUrl() + */ + +/*! \fn void Node::setUrl(const QString &url) + Sets the node's URL to \a url, which is the url to the page that the + node represents. This function is only called when an index file is + read. In other words, a node's url is set when qdoc decides where its + page will be and what its name will be, which happens when qdoc writes + the index file for the module being documented. + + \sa QDocIndexFiles + */ + +/*! + Returns this node's type as a string for use as an + attribute value in XML or HTML. + */ +QString Node::nodeTypeString() const +{ + if (isFunction()) { + const auto *fn = static_cast<const FunctionNode *>(this); + return fn->kindString(); + } + return nodeTypeString(nodeType()); +} + +/*! + Returns the node type \a t as a string for use as an + attribute value in XML or HTML. + */ +QString Node::nodeTypeString(NodeType t) +{ + switch (t) { + case Namespace: + return QLatin1String("namespace"); + case Class: + return QLatin1String("class"); + case Struct: + return QLatin1String("struct"); + case Union: + return QLatin1String("union"); + case HeaderFile: + return QLatin1String("header"); + case Page: + return QLatin1String("page"); + case Enum: + return QLatin1String("enum"); + case Example: + return QLatin1String("example"); + case ExternalPage: + return QLatin1String("external page"); + case TypeAlias: + case Typedef: + return QLatin1String("typedef"); + case Function: + return QLatin1String("function"); + case Property: + return QLatin1String("property"); + case Proxy: + return QLatin1String("proxy"); + case Variable: + return QLatin1String("variable"); + case Group: + return QLatin1String("group"); + case Module: + return QLatin1String("module"); + + case QmlType: + return QLatin1String("QML type"); + case QmlValueType: + return QLatin1String("QML value type"); + case QmlModule: + return QLatin1String("QML module"); + case QmlProperty: + return QLatin1String("QML property"); + + case SharedComment: + return QLatin1String("shared comment"); + case Collection: + return QLatin1String("collection"); + default: + break; + } + return QString(); +} + +/*! Converts the boolean value \a b to an enum representation + of the boolean type, which includes an enum value for the + \e {default value} of the item, i.e. true, false, or default. + */ +Node::FlagValue Node::toFlagValue(bool b) +{ + return b ? FlagValueTrue : FlagValueFalse; +} + +/*! + Converts the enum \a fv back to a boolean value. + If \a fv is neither the true enum value nor the + false enum value, the boolean value returned is + \a defaultValue. + */ +bool Node::fromFlagValue(FlagValue fv, bool defaultValue) +{ + switch (fv) { + case FlagValueTrue: + return true; + case FlagValueFalse: + return false; + default: + return defaultValue; + } +} + +/*! + This function creates a pair that describes a link. + The pair is composed from \a link and \a desc. The + \a linkType is the map index the pair is filed under. + */ +void Node::setLink(LinkType linkType, const QString &link, const QString &desc) +{ + std::pair<QString, QString> linkPair; + linkPair.first = link; + linkPair.second = desc; + m_linkMap[linkType] = linkPair; +} + +/*! + Sets the information about the project and version a node was introduced + in, unless the version is lower than the 'ignoresince.<project>' + configuration variable. + */ +void Node::setSince(const QString &since) +{ + QStringList parts = since.split(QLatin1Char(' ')); + QString project; + if (parts.size() > 1) + project = Config::dot + parts.first(); + + QVersionNumber cutoff = + QVersionNumber::fromString(Config::instance().get(CONFIG_IGNORESINCE + project).asString()) + .normalized(); + + if (!cutoff.isNull() && QVersionNumber::fromString(parts.last()).normalized() < cutoff) + return; + + m_since = parts.join(QLatin1Char(' ')); +} + +/*! + Extract a class name from the type \a string and return it. + */ +QString Node::extractClassName(const QString &string) const +{ + QString result; + for (int i = 0; i <= string.size(); ++i) { + QChar ch; + if (i != string.size()) + ch = string.at(i); + + QChar lower = ch.toLower(); + if ((lower >= QLatin1Char('a') && lower <= QLatin1Char('z')) || ch.digitValue() >= 0 + || ch == QLatin1Char('_') || ch == QLatin1Char(':')) { + result += ch; + } else if (!result.isEmpty()) { + if (result != QLatin1String("const")) + return result; + result.clear(); + } + } + return result; +} + +/*! + Returns the thread safeness value for whatever this node + represents. But if this node has a parent and the thread + safeness value of the parent is the same as the thread + safeness value of this node, what is returned is the + value \c{UnspecifiedSafeness}. Why? + */ +Node::ThreadSafeness Node::threadSafeness() const +{ + if (m_parent && m_safeness == m_parent->inheritedThreadSafeness()) + return UnspecifiedSafeness; + return m_safeness; +} + +/*! + If this node has a parent, the parent's thread safeness + value is returned. Otherwise, this node's thread safeness + value is returned. Why? + */ +Node::ThreadSafeness Node::inheritedThreadSafeness() const +{ + if (m_parent && m_safeness == UnspecifiedSafeness) + return m_parent->inheritedThreadSafeness(); + return m_safeness; +} + +/*! + Returns \c true if the node's status is \c Internal, or if + its parent is a class with \c Internal status. + */ +bool Node::isInternal() const +{ + if (status() == Internal) + return true; + return parent() && parent()->status() == Internal && !parent()->isAbstract(); +} + +/*! \fn void Node::markInternal() + Sets the node's access to Private and its status to Internal. + */ + +/*! + Returns a pointer to the root of the Tree this node is in. + */ +Aggregate *Node::root() const +{ + if (parent() == nullptr) + return (this->isAggregate() ? static_cast<Aggregate *>(const_cast<Node *>(this)) : nullptr); + Aggregate *t = parent(); + while (t->parent() != nullptr) + t = t->parent(); + return t; +} + +/*! + Returns a pointer to the Tree this node is in. + */ +Tree *Node::tree() const +{ + return root()->tree(); +} + +/*! + Sets the node's declaration location, its definition + location, or both, depending on the suffix of the file + name from the file path in location \a t. + */ +void Node::setLocation(const Location &t) +{ + QString suffix = t.fileSuffix(); + if (suffix == "h") + m_declLocation = t; + else if (suffix == "cpp") + m_defLocation = t; + else { + m_declLocation = t; + m_defLocation = t; + } +} + +/*! + Returns \c true if this node is documented, or it represents + a documented node read from the index ('had doc'), or this + node is sharing a non-empty doc with other nodes. + + \sa Doc + */ +bool Node::hasDoc() const +{ + if (m_hadDoc) + return true; + + if (!m_doc.isEmpty()) + return true; + + return (m_sharedCommentNode && m_sharedCommentNode->hasDoc()); +} + +/*! + Returns the CPP node's qualified name by prepending the + namespaces name + "::" if there isw a namespace. + */ +QString Node::qualifyCppName() +{ + if (m_parent && m_parent->isNamespace() && !m_parent->name().isEmpty()) + return m_parent->name() + "::" + m_name; + return m_name; +} + +/*! + Return the name of this node qualified with the parent name + and "::" if there is a parent name. + */ +QString Node::qualifyWithParentName() +{ + if (m_parent && !m_parent->name().isEmpty()) + return m_parent->name() + "::" + m_name; + return m_name; +} + +/*! + Returns the QML node's qualified name by prepending the logical + module name. + */ +QString Node::qualifyQmlName() +{ + return logicalModuleName() + "::" + m_name; +} + +/*! + Returns \c true if the node is a class node or a QML type node + that is marked as being a wrapper class or wrapper QML type, + or if it is a member of a wrapper class or type. + */ +bool Node::isWrapper() const +{ + return m_parent != nullptr && m_parent->isWrapper(); +} + +/*! + Construct the full document name for this node and return it. + */ +QString Node::fullDocumentName() const +{ + QStringList pieces; + const Node *n = this; + + do { + if (!n->name().isEmpty()) + pieces.insert(0, n->name()); + + if (n->isQmlType() && !n->logicalModuleName().isEmpty()) { + pieces.insert(0, n->logicalModuleName()); + break; + } + + if (n->isTextPageNode()) + break; + + // Examine the parent if the node is a member + if (!n->parent() || n->isRelatedNonmember()) + break; + + n = n->parent(); + } while (true); + + // Create a name based on the type of the ancestor node. + QString concatenator = "::"; + if (n->isQmlType()) + concatenator = QLatin1Char('.'); + + if (n->isTextPageNode()) + concatenator = QLatin1Char('#'); + + return pieces.join(concatenator); +} + +/*! + Sets the Node status to Node::Deprecated, unless \a sinceVersion represents + a future version. + + Stores \a sinceVersion representing the version in which the deprecation + took (or will take) place. + + Fetches the current version from the config ('version' variable) as a + string, and compared to \a sinceVersion. If both string represent a valid + version and \a sinceVersion is greater than the currect version, do not + mark the node as deprecated; leave it active. +*/ +void Node::setDeprecated(const QString &sinceVersion) +{ + + if (!m_deprecatedSince.isEmpty()) + qCWarning(lcQdoc) << QStringLiteral( + "Setting deprecated since version for %1 to %2 even though it " + "was already set to %3. This is very unexpected.") + .arg(this->m_name, sinceVersion, this->m_deprecatedSince); + m_deprecatedSince = sinceVersion; + + if (!sinceVersion.isEmpty()) { + QVersionNumber since = QVersionNumber::fromString(sinceVersion).normalized(); + QVersionNumber current = QVersionNumber::fromString( + Config::instance().get(CONFIG_VERSION).asString()) + .normalized(); + if (!current.isNull() && !since.isNull()) { + if (current < since) + return; + } + } + setStatus(Deprecated); +} + +/*! \fn Node *Node::clone(Aggregate *parent) + + When reimplemented in a subclass, this function creates a + clone of this node on the heap and makes the clone a child + of \a parent. A pointer to the clone is returned. + + Here in the base class, this function does nothing and returns + nullptr. + */ + +/*! \fn NodeType Node::nodeType() const + Returns this node's type. + + \sa NodeType +*/ + +/*! \fn Genus Node::genus() const + Returns this node's Genus. + + \sa Genus +*/ + +/*! void Node::setGenus(Genus t) + Sets this node's Genus to \a t. +*/ + +/*! \fn QString Node::signature(Node::SignatureOptions options) const + + Specific parts of the signature are included according to flags in + \a options. + + If this node is not a FunctionNode, this function returns plainName(). + + \sa FunctionNode::signature() +*/ + +/*! \fn const QString &Node::fileNameBase() const + Returns the node's file name base string, which is built once, when + Generator::fileBase() is called and stored in the Node. +*/ + +/*! \fn bool Node::hasFileNameBase() const + Returns true if the node's file name base has been set. + + \sa Node::fileNameBase() +*/ + +/*! \fn void Node::setFileNameBase(const QString &t) + Sets the node's file name base to \a t. Only called by + Generator::fileBase(). +*/ + +/*! \fn void Node::setAccess(Access t) + Sets the node's access type to \a t. + + \sa Access +*/ + +/*! \fn void Node::setThreadSafeness(ThreadSafeness t) + Sets the node's thread safeness to \a t. + + \sa ThreadSafeness +*/ + +/*! \fn void Node::setPhysicalModuleName(const QString &name) + Sets the node's physical module \a name. +*/ + +/*! \fn void Node::setReconstitutedBrief(const QString &t) + When reading an index file, this function is called with the + reconstituted brief clause \a t to set the node's brief clause. + I think this is needed for linking to something in the brief clause. +*/ + +/*! \fn void Node::setParent(Aggregate *n) + Sets the node's parent pointer to \a n. Such a thing + is not lightly done. All the calls to this function + are in other member functions of Node subclasses. See + the code in the subclass implementations to understand + when this function can be called safely and why it is called. +*/ + +/*! \fn void Node::setIndexNodeFlag(bool isIndexNode = true) + Sets a flag in this Node that indicates the node was created + for something in an index file. This is important to know + because an index node is not to be documented in the current + module. When the index flag is set, it means the Node + represents something in another module, and it will be + documented in that module's documentation. +*/ + +/*! \fn void Node::setRelatedNonmember(bool b) + Sets a flag in the node indicating whether this node is a related nonmember + of something. This function is called when the \c relates command is seen. + */ + +/*! \fn void Node::addMember(Node *node) + In a CollectionNode, this function adds \a node to the collection + node's members list. It does nothing if this node is not a CollectionNode. + */ + +/*! \fn bool Node::hasNamespaces() const + Returns \c true if this is a CollectionNode and its members list + contains namespace nodes. Otherwise it returns \c false. + */ + +/*! \fn bool Node::hasClasses() const + Returns \c true if this is a CollectionNode and its members list + contains class nodes. Otherwise it returns \c false. + */ + +/*! \fn void Node::setAbstract(bool b) + If this node is a ClassNode or a QmlTypeNode, the node's abstract flag + data member is set to \a b. + */ + +/*! \fn void Node::setWrapper() + If this node is a ClassNode or a QmlTypeNode, the node's wrapper flag + data member is set to \c true. + */ + +/*! \fn void Node::setDataType(const QString &dataType) + If this node is a PropertyNode or a QmlPropertyNode, its + data type data member is set to \a dataType. Otherwise, + this function does nothing. + */ + +/*! \fn bool Node::wasSeen() const + Returns the \c seen flag data member of this node if it is a NamespaceNode + or a CollectionNode. Otherwise it returns \c false. If \c true is returned, + it means that the location where the namespace or collection is to be + documented has been found. + */ + +/*! \fn void appendGroupName(const QString &t) + If this node is a PageNode, the group name \a t is appended to the node's + list of group names. It is not clear to me what this list of group names + is used for, but it is written to the index file, and it is used in the + navigation bar. + */ + +/*! \fn QString Node::element() const + If this node is a QmlPropertyNode or a FunctionNode, this function + returns the name of the parent node. Otherwise it returns an empty + string. + */ + +/*! \fn bool Node::docMustBeGenerated() const + This function is called to perform a test to decide if the node must have + documentation generated. In the Node base class, it always returns \c false. + + In the ProxyNode class it always returns \c true. There aren't many proxy + nodes, but when one appears, it must generate documentation. In the overrides + in NamespaceNode and ClassNode, a meaningful test is performed to decide if + documentation must be generated. + */ + +/*! \fn QString Node::title() const + Returns a string that can be used to print a title in the documentation for + whatever this Node is. In the Node base class, the node's name() is returned. + In a PageNode, the function returns the title data member. In a HeaderNode, + if the title() is empty, the name() is returned. + */ + +/*! \fn QString Node::subtitle() const { return QString(); } + Returns a string that can be used to print a subtitle in the documentation for + whatever this Node is. In the Node base class, the empty string is returned. + In a PageNode, the function returns the subtitle data member. In a HeaderNode, + the subtitle data member is returned. + */ + +/*! \fn QString Node::fullTitle() const + Returns a string that can be used as the full title for the documentation of + this node. In this base class, the name() is returned. In a PageNode, title() + is returned. In a HeaderNode, if the title() is empty, the name() is returned. + If the title() is not empty then name-title is returned. In a CollectionNode, + the title() is returned. + */ + +/*! \fn bool Node::setTitle(const QString &title) + Sets the node's \a title, which is used for the title of + the documentation page, if one is generated for this node. + Returns \c true if the title is set. In this base class, + there is no title string stored, so in the base class, + nothing happens and \c false is returned. The override in + the PageNode class is where the title is set. + */ + +/*! \fn bool Node::setSubtitle(const QString &subtitle) + Sets the node's \a subtitle, which is used for the subtitle + of the documentation page, if one is generated for this node. + Returns \c true if the subtitle is set. In this base class, + there is no subtitle string stored, so in the base class, + nothing happens and \c false is returned. The override in + the PageNode and HeaderNode classes is where the subtitle is + set. + */ + +/*! \fn void Node::markDefault() + If this node is a QmlPropertyNode, it is marked as the default property. + Otherwise the function does nothing. + */ + +/*! \fn void Node::markReadOnly(bool flag) + If this node is a QmlPropertyNode, then the property's read-only + flag is set to \a flag. + */ + +/*! \fn Aggregate *Node::parent() const + Returns the node's parent pointer. +*/ + +/*! \fn const QString &Node::name() const + Returns the node's name data member. +*/ + +/*! \fn void Node::setQtVariable(const QString &v) + If this node is a CollectionNode, its QT variable is set to \a v. + Otherwise the function does nothing. I don't know what the QT variable + is used for. + */ + +/*! \fn QString Node::qtVariable() const + If this node is a CollectionNode, its QT variable is returned. + Otherwise an empty string is returned. I don't know what the QT + variable is used for. + */ + +/*! \fn bool Node::hasTag(const QString &t) const + If this node is a FunctionNode, the function returns \c true if + the function has the tag \a t. Otherwise the function returns + \c false. I don't know what the tag is used for. + */ + +/*! \fn const QMap<LinkType, std::pair<QString, QString> > &Node::links() const + Returns a reference to this node's link map. The link map should + probably be moved to the PageNode, because it contains links to the + start page, next page, previous page, and contents page, and these + are only used in PageNode, I think. + */ + +/*! \fn Access Node::access() const + Returns the node's Access setting, which can be \c Public, + \c Protected, or \c Private. + */ + +/*! \fn const Location& Node::declLocation() const + Returns the Location where this node's declaration was seen. + Normally the declaration location is in an \e include file. + The declaration location is used in qdoc error/warning messages + about the declaration. + */ + +/*! \fn const Location& Node::defLocation() const + Returns the Location where this node's dedefinition was seen. + Normally the definition location is in a \e .cpp file. + The definition location is used in qdoc error/warning messages + when the error is discovered at the location of the definition, + although the way to correct the problem often requires changing + the declaration. + */ + +/*! \fn const Location& Node::location() const + If this node's definition location is empty, this function + returns this node's declaration location. Otherwise it + returns the definition location. + + \sa Location + */ + +/*! \fn const Doc &Node::doc() const + Returns a reference to the node's Doc data member. + + \sa Doc + */ + +/*! \fn Status Node::status() const + Returns the node's status value. + + \sa Status + */ + +/*! \fn QString Node::since() const + Returns the node's since string, which can be empty. + */ + +/*! \fn QString Node::templateStuff() const + Returns the node's template parameters string, if this node + represents a templated element. + */ + +/*! \fn bool Node::isSharingComment() const + This function returns \c true if the node is sharing a comment + with other nodes. For example, multiple functions can be documented + with a single qdoc comment by listing the \c {\\fn} signatures for + all the functions in the single qdoc comment. + */ + +/*! \fn QString Node::qmlTypeName() const + If this is a QmlPropertyNode or a FunctionNode representing a QML + method, this function returns the qmlTypeName() of + the parent() node. Otherwise it returns the name data member. + */ + +/*! \fn QString Node::qmlFullBaseName() const + If this is a QmlTypeNode, this function returns the QML full + base name. Otherwise it returns an empty string. + */ + +/*! \fn QString Node::logicalModuleName() const + If this is a CollectionNode, this function returns the logical + module name. Otherwise it returns an empty string. + */ + +/*! \fn QString Node::logicalModuleVersion() const + If this is a CollectionNode, this function returns the logical + module version number. Otherwise it returns an empty string. + */ + +/*! \fn QString Node::logicalModuleIdentifier() const + If this is a CollectionNode, this function returns the logical + module identifier. Otherwise it returns an empty string. + */ + +/*! \fn void Node::setLogicalModuleInfo(const QString &arg) + If this node is a CollectionNode, this function splits \a arg + on the blank character to get a logical module name and version + number. If the version number is present, it splits the version + number on the '.' character to get a major version number and a + minor version number. If the version number is present, both the + major and minor version numbers should be there, but the minor + version number is not absolutely necessary. + + The strings are stored in the appropriate data members for use + when the QML module page is generated. + */ + +/*! \fn void Node::setLogicalModuleInfo(const QStringList &info) + If this node is a CollectionNode, this function accepts the + logical module \a info as a string list. If the logical module + info contains the version number, it splits the version number + on the '.' character to get the major and minor version numbers. + Both major and minor version numbers should be provided, but + the minor version number is not strictly necessary. + + The strings are stored in the appropriate data members for use + when the QML module page is generated. This overload + of the function is called when qdoc is reading an index file. + */ + +/*! \fn CollectionNode *Node::logicalModule() const + If this is a QmlTypeNode, a pointer to its QML module is returned, + which is a pointer to a CollectionNode. Otherwise the \c nullptr + is returned. + */ + +/*! \fn void Node::setQmlModule(CollectionNode *t) + If this is a QmlTypeNode, this function sets the QML type's QML module + pointer to the CollectionNode \a t. Otherwise the function does nothing. + */ + +/*! \fn ClassNode *Node::classNode() + If this is a QmlTypeNode, this function returns the pointer to + the C++ ClassNode that this QML type represents. Otherwise the + \c nullptr is returned. + */ + +/*! \fn void Node::setClassNode(ClassNode *cn) + If this is a QmlTypeNode, this function sets the C++ class node + to \a cn. The C++ ClassNode is the C++ implementation of the QML + type. + */ + +/*! \fn NodeType Node::goal(const QString &t) + When a square-bracket parameter is used in a qdoc command, this + function might be called to convert the text string \a t obtained + from inside the square brackets to be a Goal value, which is returned. + + \sa Goal + */ + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/node.h b/src/qdoc/qdoc/src/qdoc/node.h new file mode 100644 index 000000000..faf06d51c --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/node.h @@ -0,0 +1,344 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef NODE_H +#define NODE_H + +#include "access.h" +#include "comparisoncategory.h" +#include "doc.h" +#include "enumitem.h" +#include "importrec.h" +#include "parameters.h" +#include "relatedclass.h" +#include "template_declaration.h" + +#include <QtCore/qdir.h> +#include <QtCore/qlist.h> +#include <QtCore/qmap.h> +#include <QtCore/qstringlist.h> + +#include <optional> + +QT_BEGIN_NAMESPACE + +class Aggregate; +class ClassNode; +class CollectionNode; +class EnumNode; +class ExampleNode; +class FunctionNode; +class Node; +class QDocDatabase; +class QmlTypeNode; +class PageNode; +class PropertyNode; +class QmlPropertyNode; +class SharedCommentNode; +class Tree; +class TypedefNode; + +typedef QList<Node *> NodeList; +typedef QList<ClassNode *> ClassList; +typedef QList<Node *> NodeVector; +typedef QMap<QString, Node *> NodeMap; +typedef QMap<QString, NodeMap> NodeMapMap; +typedef QMultiMap<QString, Node *> NodeMultiMap; +typedef QMap<QString, NodeMultiMap> NodeMultiMapMap; +typedef QMap<QString, CollectionNode *> CNMap; +typedef QMultiMap<QString, CollectionNode *> CNMultiMap; + +class Node +{ +public: + enum NodeType : unsigned char { + NoType, + Namespace, + Class, + Struct, + Union, + HeaderFile, + Page, + Enum, + Example, + ExternalPage, + Function, + Typedef, + TypeAlias, + Property, + Variable, + Group, + Module, + QmlType, + QmlModule, + QmlProperty, + QmlValueType, + SharedComment, + Collection, + Proxy + }; + + enum Genus : unsigned char { + DontCare = 0x0, + CPP = 0x1, + QML = 0x4, + DOC = 0x8, + API = CPP | QML + }; + + enum Status : unsigned char { + Deprecated, + Preliminary, + Active, + Internal, + DontDocument + }; // don't reorder this enum + + enum ThreadSafeness : unsigned char { + UnspecifiedSafeness, + NonReentrant, + Reentrant, + ThreadSafe + }; + + enum SignatureOption : unsigned char { + SignaturePlain = 0x0, + SignatureDefaultValues = 0x1, + SignatureReturnType = 0x2, + SignatureTemplateParams = 0x4 + }; + Q_DECLARE_FLAGS(SignatureOptions, SignatureOption) + + enum LinkType : unsigned char { StartLink, NextLink, PreviousLink, ContentsLink }; + + enum FlagValue { FlagValueDefault = -1, FlagValueFalse = 0, FlagValueTrue = 1 }; + + virtual ~Node() = default; + virtual Node *clone(Aggregate *) { return nullptr; } // currently only FunctionNode + [[nodiscard]] virtual Tree *tree() const; + [[nodiscard]] Aggregate *root() const; + + [[nodiscard]] NodeType nodeType() const { return m_nodeType; } + [[nodiscard]] QString nodeTypeString() const; + + [[nodiscard]] Genus genus() const { return m_genus; } + void setGenus(Genus t) { m_genus = t; } + static Genus getGenus(NodeType t); + + [[nodiscard]] bool isActive() const { return m_status == Active; } + [[nodiscard]] bool isClass() const { return m_nodeType == Class; } + [[nodiscard]] bool isCppNode() const { return genus() == CPP; } + [[nodiscard]] bool isDontDocument() const { return (m_status == DontDocument); } + [[nodiscard]] bool isEnumType() const { return m_nodeType == Enum; } + [[nodiscard]] bool isExample() const { return m_nodeType == Example; } + [[nodiscard]] bool isExternalPage() const { return m_nodeType == ExternalPage; } + [[nodiscard]] bool isFunction(Genus g = DontCare) const + { + return m_nodeType == Function && (genus() == g || g == DontCare); + } + [[nodiscard]] bool isGroup() const { return m_nodeType == Group; } + [[nodiscard]] bool isHeader() const { return m_nodeType == HeaderFile; } + [[nodiscard]] bool isIndexNode() const { return m_indexNodeFlag; } + [[nodiscard]] bool isModule() const { return m_nodeType == Module; } + [[nodiscard]] bool isNamespace() const { return m_nodeType == Namespace; } + [[nodiscard]] bool isPage() const { return m_nodeType == Page; } + [[nodiscard]] bool isPreliminary() const { return (m_status == Preliminary); } + [[nodiscard]] bool isPrivate() const { return m_access == Access::Private; } + [[nodiscard]] bool isProperty() const { return m_nodeType == Property; } + [[nodiscard]] bool isProxyNode() const { return m_nodeType == Proxy; } + [[nodiscard]] bool isPublic() const { return m_access == Access::Public; } + [[nodiscard]] bool isProtected() const { return m_access == Access::Protected; } + [[nodiscard]] bool isQmlBasicType() const { return m_nodeType == QmlValueType; } + [[nodiscard]] bool isQmlModule() const { return m_nodeType == QmlModule; } + [[nodiscard]] bool isQmlNode() const { return genus() == QML; } + [[nodiscard]] bool isQmlProperty() const { return m_nodeType == QmlProperty; } + [[nodiscard]] bool isQmlType() const { return m_nodeType == QmlType || m_nodeType == QmlValueType; } + [[nodiscard]] bool isRelatedNonmember() const { return m_relatedNonmember; } + [[nodiscard]] bool isStruct() const { return m_nodeType == Struct; } + [[nodiscard]] bool isSharedCommentNode() const { return m_nodeType == SharedComment; } + [[nodiscard]] bool isTypeAlias() const { return m_nodeType == TypeAlias; } + [[nodiscard]] bool isTypedef() const + { + return m_nodeType == Typedef || m_nodeType == TypeAlias; + } + [[nodiscard]] bool isUnion() const { return m_nodeType == Union; } + [[nodiscard]] bool isVariable() const { return m_nodeType == Variable; } + [[nodiscard]] bool isGenericCollection() const { return (m_nodeType == Node::Collection); } + + [[nodiscard]] virtual bool isDeprecated() const { return (m_status == Deprecated); } + [[nodiscard]] virtual bool isAbstract() const { return false; } + [[nodiscard]] virtual bool isAggregate() const { return false; } // means "can have children" + [[nodiscard]] virtual bool isFirstClassAggregate() const + { + return false; + } // Aggregate but not proxy or prop group" + [[nodiscard]] virtual bool isAlias() const { return false; } + [[nodiscard]] virtual bool isAttached() const { return false; } + [[nodiscard]] virtual bool isClassNode() const { return false; } + [[nodiscard]] virtual bool isCollectionNode() const { return false; } + [[nodiscard]] virtual bool isDefault() const { return false; } + [[nodiscard]] virtual bool isInternal() const; + [[nodiscard]] virtual bool isMacro() const { return false; } + [[nodiscard]] virtual bool isPageNode() const { return false; } // means "generates a doc page" + [[nodiscard]] virtual bool isRelatableType() const { return false; } + [[nodiscard]] virtual bool isMarkedReimp() const { return false; } + [[nodiscard]] virtual bool isPropertyGroup() const { return false; } + [[nodiscard]] virtual bool isStatic() const { return false; } + [[nodiscard]] virtual bool isTextPageNode() const + { + return false; + } // means PageNode but not Aggregate + [[nodiscard]] virtual bool isWrapper() const; + + [[nodiscard]] QString plainName() const; + QString plainFullName(const Node *relative = nullptr) const; + [[nodiscard]] QString plainSignature() const; + QString fullName(const Node *relative = nullptr) const; + [[nodiscard]] virtual QString signature(Node::SignatureOptions) const { return plainName(); } + + [[nodiscard]] const QString &fileNameBase() const { return m_fileNameBase; } + [[nodiscard]] bool hasFileNameBase() const { return !m_fileNameBase.isEmpty(); } + void setFileNameBase(const QString &t) { m_fileNameBase = t; } + + void setAccess(Access t) { m_access = t; } + void setLocation(const Location &t); + void setDoc(const Doc &doc, bool replace = false); + void setStatus(Status t); + void setThreadSafeness(ThreadSafeness t) { m_safeness = t; } + void setSince(const QString &since); + void setPhysicalModuleName(const QString &name) { m_physicalModuleName = name; } + void setUrl(const QString &url) { m_url = url; } + void setTemplateDecl(std::optional<RelaxedTemplateDeclaration> t) { m_templateDecl = t; } + void setReconstitutedBrief(const QString &t) { m_reconstitutedBrief = t; } + void setParent(Aggregate *n) { m_parent = n; } + void setIndexNodeFlag(bool isIndexNode = true) { m_indexNodeFlag = isIndexNode; } + void setHadDoc() { m_hadDoc = true; } + void setComparisonCategory(const ComparisonCategory &category) { m_comparisonCategory = category; } + [[nodiscard]] ComparisonCategory comparisonCategory() const { return m_comparisonCategory; } + virtual void setRelatedNonmember(bool b) { m_relatedNonmember = b; } + virtual void addMember(Node *) {} + [[nodiscard]] virtual bool hasNamespaces() const { return false; } + [[nodiscard]] virtual bool hasClasses() const { return false; } + virtual void setAbstract(bool) {} + virtual void setWrapper() {} + virtual void setDataType(const QString &) {} + [[nodiscard]] virtual bool wasSeen() const { return false; } + virtual void appendGroupName(const QString &) {} + [[nodiscard]] virtual QString element() const { return QString(); } + [[nodiscard]] virtual bool docMustBeGenerated() const { return false; } + + [[nodiscard]] virtual QString title() const { return name(); } + [[nodiscard]] virtual QString subtitle() const { return QString(); } + [[nodiscard]] virtual QString fullTitle() const { return name(); } + virtual bool setTitle(const QString &) { return false; } + virtual bool setSubtitle(const QString &) { return false; } + + void markInternal() + { + setAccess(Access::Private); + setStatus(Internal); + } + virtual void markDefault() {} + virtual void markReadOnly(bool) {} + + [[nodiscard]] Aggregate *parent() const { return m_parent; } + [[nodiscard]] const QString &name() const { return m_name; } + [[nodiscard]] QString physicalModuleName() const { return m_physicalModuleName; } + [[nodiscard]] QString url() const { return m_url; } + virtual void setQtVariable(const QString &) {} + [[nodiscard]] virtual QString qtVariable() const { return QString(); } + virtual void setQtCMakeComponent(const QString &) {} + virtual void setQtCMakeTargetItem(const QString &) {} + [[nodiscard]] virtual QString qtCMakeComponent() const { return QString(); } + [[nodiscard]] virtual QString qtCMakeTargetItem() const { return QString(); } + [[nodiscard]] virtual bool hasTag(const QString &) const { return false; } + + void setDeprecated(const QString &sinceVersion); + [[nodiscard]] const QString &deprecatedSince() const { return m_deprecatedSince; } + + [[nodiscard]] const QMap<LinkType, std::pair<QString, QString>> &links() const { return m_linkMap; } + void setLink(LinkType linkType, const QString &link, const QString &desc); + + [[nodiscard]] Access access() const { return m_access; } + [[nodiscard]] const Location &declLocation() const { return m_declLocation; } + [[nodiscard]] const Location &defLocation() const { return m_defLocation; } + [[nodiscard]] const Location &location() const + { + return (m_defLocation.isEmpty() ? m_declLocation : m_defLocation); + } + [[nodiscard]] const Doc &doc() const { return m_doc; } + [[nodiscard]] bool isInAPI() const + { + return !isPrivate() && !isInternal() && !isDontDocument() && hasDoc(); + } + [[nodiscard]] bool hasDoc() const; + [[nodiscard]] bool hadDoc() const { return m_hadDoc; } + [[nodiscard]] Status status() const { return m_status; } + [[nodiscard]] ThreadSafeness threadSafeness() const; + [[nodiscard]] ThreadSafeness inheritedThreadSafeness() const; + [[nodiscard]] QString since() const { return m_since; } + [[nodiscard]] const std::optional<RelaxedTemplateDeclaration>& templateDecl() const { return m_templateDecl; } + [[nodiscard]] const QString &reconstitutedBrief() const { return m_reconstitutedBrief; } + + [[nodiscard]] bool isSharingComment() const { return (m_sharedCommentNode != nullptr); } + void setSharedCommentNode(SharedCommentNode *t) { m_sharedCommentNode = t; } + SharedCommentNode *sharedCommentNode() { return m_sharedCommentNode; } + + [[nodiscard]] QString extractClassName(const QString &string) const; + [[nodiscard]] virtual QString qmlTypeName() const { return m_name; } + [[nodiscard]] virtual QString qmlFullBaseName() const { return QString(); } + [[nodiscard]] virtual QString logicalModuleName() const { return QString(); } + [[nodiscard]] virtual QString logicalModuleVersion() const { return QString(); } + [[nodiscard]] virtual QString logicalModuleIdentifier() const { return QString(); } + + virtual void setLogicalModuleInfo(const QStringList &) {} + [[nodiscard]] virtual CollectionNode *logicalModule() const { return nullptr; } + virtual void setQmlModule(CollectionNode *) {} + virtual ClassNode *classNode() { return nullptr; } + virtual void setClassNode(ClassNode *) {} + [[nodiscard]] QString fullDocumentName() const; + QString qualifyCppName(); + QString qualifyQmlName(); + QString qualifyWithParentName(); + + static FlagValue toFlagValue(bool b); + static bool fromFlagValue(FlagValue fv, bool defaultValue); + static QString nodeTypeString(NodeType t); + [[nodiscard]] static bool nodeNameLessThan(const Node *first, const Node *second); + [[nodiscard]] static bool nodeSortKeyOrNameLessThan(const Node *n1, const Node *n2); + +protected: + Node(NodeType type, Aggregate *parent, QString name); + +private: + NodeType m_nodeType {}; + Genus m_genus {}; + Access m_access { Access::Public }; + ThreadSafeness m_safeness { UnspecifiedSafeness }; + Status m_status { Active }; + ComparisonCategory m_comparisonCategory { ComparisonCategory::None }; + bool m_indexNodeFlag : 1; + bool m_relatedNonmember : 1; + bool m_hadDoc : 1; + + Aggregate *m_parent { nullptr }; + SharedCommentNode *m_sharedCommentNode { nullptr }; + QString m_name {}; + Location m_declLocation {}; + Location m_defLocation {}; + Doc m_doc {}; + QMap<LinkType, std::pair<QString, QString>> m_linkMap {}; + QString m_fileNameBase {}; + QString m_physicalModuleName {}; + QString m_url {}; + QString m_since {}; + std::optional<RelaxedTemplateDeclaration> m_templateDecl{std::nullopt}; + QString m_reconstitutedBrief {}; + QString m_deprecatedSince {}; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(Node::SignatureOptions) + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/openedlist.cpp b/src/qdoc/qdoc/src/qdoc/openedlist.cpp new file mode 100644 index 000000000..a85e45ec4 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/openedlist.cpp @@ -0,0 +1,172 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "openedlist.h" + +#include "atom.h" + +#include <QtCore/qregularexpression.h> + +QT_BEGIN_NAMESPACE + +static const char roman[] = "m\2d\5c\2l\5x\2v\5i"; + +OpenedList::OpenedList(ListStyle style) : sty(style), ini(1), nex(0) {} + +OpenedList::OpenedList(const Location &location, const QString &hint) : sty(Bullet), ini(1) +{ + static const QRegularExpression hintSyntax("^(\\W*)([0-9]+|[A-Z]+|[a-z]+)(\\W*)$"); + + auto match = hintSyntax.match(hint); + if (match.hasMatch()) { + bool ok; + int asNumeric = hint.toInt(&ok); + int asRoman = fromRoman(match.captured(2)); + int asAlpha = fromAlpha(match.captured(2)); + + if (ok) { + sty = Numeric; + ini = asNumeric; + } else if (asRoman > 0 && asRoman != 100 && asRoman != 500) { + sty = (hint == hint.toLower()) ? LowerRoman : UpperRoman; + ini = asRoman; + } else { + sty = (hint == hint.toLower()) ? LowerAlpha : UpperAlpha; + ini = asAlpha; + } + pref = match.captured(1); + suff = match.captured(3); + } else if (!hint.isEmpty()) { + location.warning(QStringLiteral("Unrecognized list style '%1'").arg(hint)); + } + nex = ini - 1; +} + +QString OpenedList::styleString() const +{ + switch (style()) { + case Bullet: + default: + return ATOM_LIST_BULLET; + case Tag: + return ATOM_LIST_TAG; + case Value: + return ATOM_LIST_VALUE; + case Numeric: + return ATOM_LIST_NUMERIC; + case UpperAlpha: + return ATOM_LIST_UPPERALPHA; + case LowerAlpha: + return ATOM_LIST_LOWERALPHA; + case UpperRoman: + return ATOM_LIST_UPPERROMAN; + case LowerRoman: + return ATOM_LIST_LOWERROMAN; + } +} + +QString OpenedList::numberString() const +{ + return QString::number(number()); + /* + switch ( style() ) { + case Numeric: + return QString::number( number() ); + case UpperAlpha: + return toAlpha( number() ).toUpper(); + case LowerAlpha: + return toAlpha( number() ); + case UpperRoman: + return toRoman( number() ).toUpper(); + case LowerRoman: + return toRoman( number() ); + case Bullet: + default: + return "*"; + }*/ +} + +int OpenedList::fromAlpha(const QString &str) +{ + int n = 0; + int u; + + for (const QChar &character : str) { + u = character.toLower().unicode(); + if (u >= 'a' && u <= 'z') { + n *= 26; + n += u - 'a' + 1; + } else { + return 0; + } + } + return n; +} + +QString OpenedList::toRoman(int n) +{ + /* + See p. 30 of Donald E. Knuth's "TeX: The Program". + */ + QString str; + int j = 0; + int k; + int u; + int v = 1000; + + for (;;) { + while (n >= v) { + str += roman[j]; + n -= v; + } + + if (n <= 0) + break; + + k = j + 2; + u = v / roman[k - 1]; + if (roman[k - 1] == 2) { + k += 2; + u /= 5; + } + if (n + u >= v) { + str += roman[k]; + n += u; + } else { + j += 2; + v /= roman[j - 1]; + } + } + return str; +} + +int OpenedList::fromRoman(const QString &str) +{ + int n = 0; + int j; + int u; + int v = 0; + + for (const QChar &character : str) { + j = 0; + u = 1000; + while (roman[j] != 'i' && roman[j] != character.toLower()) { + j += 2; + u /= roman[j - 1]; + } + if (u < v) { + n -= u; + } else { + n += u; + } + v = u; + } + + if (str.toLower() == toRoman(n)) { + return n; + } else { + return 0; + } +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/openedlist.h b/src/qdoc/qdoc/src/qdoc/openedlist.h new file mode 100644 index 000000000..cd0c54e40 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/openedlist.h @@ -0,0 +1,47 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef OPENEDLIST_H +#define OPENEDLIST_H + +#include "location.h" + +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +class OpenedList +{ +public: + enum ListStyle { Bullet, Tag, Value, Numeric, UpperAlpha, LowerAlpha, UpperRoman, LowerRoman }; + + OpenedList() : sty(Bullet), ini(1), nex(0) {} + explicit OpenedList(ListStyle style); + OpenedList(const Location &location, const QString &hint); + + void next() { nex++; } + + [[nodiscard]] bool isStarted() const { return nex >= ini; } + [[nodiscard]] ListStyle style() const { return sty; } + [[nodiscard]] QString styleString() const; + [[nodiscard]] int number() const { return nex; } + [[nodiscard]] QString numberString() const; + [[nodiscard]] QString prefix() const { return pref; } + [[nodiscard]] QString suffix() const { return suff; } + +private: + static int fromAlpha(const QString &str); + static QString toRoman(int n); + static int fromRoman(const QString &str); + + ListStyle sty; + int ini; + int nex; + QString pref; + QString suff; +}; +Q_DECLARE_TYPEINFO(OpenedList, Q_RELOCATABLE_TYPE); + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/pagenode.cpp b/src/qdoc/qdoc/src/qdoc/pagenode.cpp new file mode 100644 index 000000000..5f01bc24f --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/pagenode.cpp @@ -0,0 +1,117 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "pagenode.h" + +#include "aggregate.h" + +QT_BEGIN_NAMESPACE + +/*! + \class PageNode + \brief A PageNode is a Node that generates a documentation page. + + Not all subclasses of Node produce documentation pages. FunctionNode, + PropertyNode, and EnumNode are all examples of subclasses of Node that + don't produce documentation pages but add documentation to a page. + They are always child nodes of an Aggregate, and Aggregate inherits + PageNode. + + Not every subclass of PageNode inherits Aggregate. ExternalPageNode, + ExampleNode, and CollectionNode are subclasses of PageNode that are + not subclasses of Aggregate. Because they are not subclasses of + Aggregate, they can't have children. But they still generate, or + link to, a documentation page. + */ + +/*! \fn QString PageNode::title() const + Returns the node's title, which is used for the page title. + */ + +/*! \fn QString PageNode::subtitle() const + Returns the node's subtitle, which may be empty. + */ + +/*! + Returns the node's full title. + */ +QString PageNode::fullTitle() const +{ + return title(); +} + +/*! + Sets the node's \a title, which is used for the page title. + Returns true. Adds the node to the parent() nonfunction map + using the \a title as the key. + */ +bool PageNode::setTitle(const QString &title) +{ + m_title = title; + parent()->addChildByTitle(this, title); + return true; +} + +/*! + \fn bool PageNode::setSubtitle(const QString &subtitle) + Sets the node's \a subtitle. Returns true; + */ + +/*! \fn PageNode::PageNode(Aggregate *parent, const QString &name) + This constructor sets the PageNode's \a parent and the \a name is the + argument of the \c {\\page} command. The node type is set to Node::Page. + */ + +/*! \fn PageNode::PageNode(NodeType type, Aggregate *parent, const QString &name) + This constructor is not called directly. It is called by the constructors of + subclasses of PageNode, usually Aggregate. The node type is set to \a type, + and the parent pointer is set to \a parent. \a name is the argument of the topic + command that resulted in the PageNode being created. This could be \c {\\class} + or \c {\\namespace}, for example. + */ + +/*! \fn PageNode::~PageNode() + The destructor is virtual, and it does nothing. + */ + +/*! \fn bool PageNode::isPageNode() const + Always returns \c true because this is a PageNode. + */ + +/*! \fn bool PageNode::isTextPageNode() const + Returns \c true if this instance of PageNode is not an Aggregate. + The significance of a \c true return value is that this PageNode + doesn't have children, because it is not an Aggregate. + + \sa Aggregate. + */ + +/*! \fn QString PageNode::imageFileName() const + If this PageNode is an ExampleNode, the image file name + data member is returned. Otherwise an empty string is + returned. + */ + +/*! \fn void PageNode::setImageFileName(const QString &ifn) + If this PageNode is an ExampleNode, the image file name + data member is set to \a ifn. Otherwise the function does + nothing. + */ + +/*! \fn bool PageNode::noAutoList() const + Returns the value of the no auto-list flag. + */ + +/*! \fn void PageNode::setNoAutoList(bool b) + Sets the no auto-list flag to \a b. + */ + +/*! \fn const QStringList &PageNode::groupNames() const + Returns a const reference to the string list containing all the group names. + */ + +/*! \fn void PageNode::appendGroupName(const QString &t) + Appends \a t to the list of group names. + */ + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/pagenode.h b/src/qdoc/qdoc/src/qdoc/pagenode.h new file mode 100644 index 000000000..14231bccd --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/pagenode.h @@ -0,0 +1,82 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef PAGENODE_H +#define PAGENODE_H + +#include "node.h" + +#include <QtCore/qglobal.h> +#include <QtCore/qstring.h> +#include <QtCore/qstringlist.h> + +QT_BEGIN_NAMESPACE + +class Aggregate; + +class PageNode : public Node +{ +public: + PageNode(Aggregate *parent, const QString &name) : Node(Page, parent, name) {} + PageNode(NodeType type, Aggregate *parent, const QString &name) : Node(type, parent, name) {} + + [[nodiscard]] bool isPageNode() const override { return true; } + [[nodiscard]] bool isTextPageNode() const override + { + return !isAggregate(); + } // PageNode but not Aggregate + + [[nodiscard]] QString title() const override { return m_title; } + [[nodiscard]] QString subtitle() const override { return m_subtitle; } + [[nodiscard]] QString fullTitle() const override; + bool setTitle(const QString &title) override; + bool setSubtitle(const QString &subtitle) override + { + m_subtitle = subtitle; + return true; + } + [[nodiscard]] virtual QString imageFileName() const { return QString(); } + virtual void setImageFileName(const QString &) {} + + [[nodiscard]] bool noAutoList() const { return m_noAutoList; } + void setNoAutoList(bool b) { m_noAutoList = b; } + [[nodiscard]] const QStringList &groupNames() const { return m_groupNames; } + void appendGroupName(const QString &t) override { m_groupNames.append(t); } + + [[nodiscard]] const PageNode *navigationParent() const { return m_navParent; } + void setNavigationParent(const PageNode *parent) { m_navParent = parent; } + + void markAttribution() { is_attribution = true; } + [[nodiscard]] bool isAttribution() const { return is_attribution; } + +protected: + friend class Node; + +protected: + bool m_noAutoList { false }; + QString m_title {}; + QString m_subtitle {}; + QStringList m_groupNames {}; + + // Marks the PageNode as being or not being an attribution. + // A PageNode that is an attribution represents a page that serves + // to present the third party software that a project uses, + // together with its license, link to the website of the project + // and so on. + // PageNode that are attribution are expected to be generate only + // for the Qt project by the QAttributionScanner, as part of the + // built of Qt's documentation. + // + // PageNodes that are attribution are marked primarily so that + // QDoc is able to generate a specialized list of attributions for + // a specific module through the use of the "\generatedlist" + // command, and behave like any other PageNode otherwise. + bool is_attribution{ false }; + +private: + const PageNode *m_navParent { nullptr }; +}; + +QT_END_NAMESPACE + +#endif // PAGENODE_H diff --git a/src/qdoc/qdoc/src/qdoc/parameters.cpp b/src/qdoc/qdoc/src/qdoc/parameters.cpp new file mode 100644 index 000000000..39f88b48f --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/parameters.cpp @@ -0,0 +1,542 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "parameters.h" + +#include "codechunk.h" +#include "generator.h" +#include "tokenizer.h" + +QT_BEGIN_NAMESPACE + +QRegularExpression Parameters::s_varComment(R"(^/\*\s*([a-zA-Z_0-9]+)\s*\*/$)"); + +/*! + \class Parameter + \brief The Parameter class describes one function parameter. + + A parameter can be a function parameter or a macro parameter. + It has a name, a data type, and an optional default value. + These are all stored as strings so they can be compared with + a parameter in a function signature to find a match. + */ + +/*! + \fn Parameter::Parameter(const QString &type, const QString &name, const QString &defaultValue) + + Constructs the parameter from the \a type, the optional \a name, + and the optional \a defaultValue. + */ + +/*! + Reconstructs the text signature for the parameter and returns + it. If \a includeValue is true and there is a default value, + the default value is appended with '='. + */ +QString Parameter::signature(bool includeValue) const +{ + QString p = m_type; + if (!p.isEmpty() && !p.endsWith(QChar('*')) && !p.endsWith(QChar('&')) && + !p.endsWith(QChar(' ')) && !m_name.isEmpty()) { + p += QLatin1Char(' '); + } + p += m_name; + if (includeValue && !m_defaultValue.isEmpty()) + p += " = " + m_defaultValue; + return p; +} + +/*! + \class Parameters + + \brief A class for parsing and managing a function parameter list + + The constructor is passed a string that is the text inside the + parentheses of a function declaration. The constructor parses + the parameter list into a vector of class Parameter. + + The Parameters object is then used in function searches to find + the correct function node given the function name and the signature + of its parameters. + */ + +Parameters::Parameters() : m_valid(true), m_privateSignal(false), m_tok(0), m_tokenizer(nullptr) +{ + // nothing. +} + +Parameters::Parameters(const QString &signature) + : m_valid(true), m_privateSignal(false), m_tok(0), m_tokenizer(nullptr) +{ + if (!signature.isEmpty()) { + if (!parse(signature)) { + m_parameters.clear(); + m_valid = false; + } + } +} + +/*! + Get the next token from the string being parsed and store + it in the token variable. + */ +void Parameters::readToken() +{ + m_tok = m_tokenizer->getToken(); +} + +/*! + Return the current lexeme from the string being parsed. + */ +QString Parameters::lexeme() +{ + return m_tokenizer->lexeme(); +} + +/*! + Return the previous lexeme read from the string being parsed. + */ +QString Parameters::previousLexeme() +{ + return m_tokenizer->previousLexeme(); +} + +/*! + If the current token is \a target, read the next token and + return \c true. Otherwise, return false without reading the + next token. + */ +bool Parameters::match(int target) +{ + if (m_tok == target) { + readToken(); + return true; + } + return false; +} + +/*! + Match a template clause in angle brackets, append it to the + \a type, and return \c true. If there is no template clause, + or if an error is detected, return \c false. + */ +void Parameters::matchTemplateAngles(CodeChunk &type) +{ + if (m_tok == Tok_LeftAngle) { + int leftAngleDepth = 0; + int parenAndBraceDepth = 0; + do { + if (m_tok == Tok_LeftAngle) { + leftAngleDepth++; + } else if (m_tok == Tok_RightAngle) { + leftAngleDepth--; + } else if (m_tok == Tok_LeftParen || m_tok == Tok_LeftBrace) { + ++parenAndBraceDepth; + } else if (m_tok == Tok_RightParen || m_tok == Tok_RightBrace) { + if (--parenAndBraceDepth < 0) + return; + } + type.append(lexeme()); + readToken(); + } while (leftAngleDepth > 0 && m_tok != Tok_Eoi); + } +} + +/*! + Uses the current tokenizer to parse the \a name and \a type + of the parameter. + */ +bool Parameters::matchTypeAndName(CodeChunk &type, QString &name) +{ + /* + This code is really hard to follow... sorry. The loop is there to match + Alpha::Beta::Gamma::...::Omega. + */ + for (;;) { + bool virgin = true; + + if (m_tok != Tok_Ident) { + /* + There is special processing for 'Foo::operator int()' + and such elsewhere. This is the only case where we + return something with a trailing gulbrandsen ('Foo::'). + */ + if (m_tok == Tok_operator) + return true; + + /* + People may write 'const unsigned short' or + 'short unsigned const' or any other permutation. + */ + while (match(Tok_const) || match(Tok_volatile)) + type.append(previousLexeme()); + QString pending; + while (m_tok == Tok_signed || m_tok == Tok_int || m_tok == Tok_unsigned + || m_tok == Tok_short || m_tok == Tok_long || m_tok == Tok_int64) { + if (m_tok == Tok_signed) + pending = lexeme(); + else { + if (m_tok == Tok_unsigned && !pending.isEmpty()) + type.append(pending); + pending.clear(); + type.append(lexeme()); + } + readToken(); + virgin = false; + } + if (!pending.isEmpty()) { + type.append(pending); + pending.clear(); + } + while (match(Tok_const) || match(Tok_volatile)) + type.append(previousLexeme()); + + if (match(Tok_Tilde)) + type.append(previousLexeme()); + } + + if (virgin) { + if (match(Tok_Ident)) { + /* + This is a hack until we replace this "parser" + with the real one used in Qt Creator. + Is it still needed? mws 11/12/2018 + */ + if (lexeme() == "(" + && ((previousLexeme() == "QT_PREPEND_NAMESPACE") + || (previousLexeme() == "NS"))) { + readToken(); + readToken(); + type.append(previousLexeme()); + readToken(); + } else + type.append(previousLexeme()); + } else if (match(Tok_void) || match(Tok_int) || match(Tok_char) || match(Tok_double) + || match(Tok_Ellipsis)) { + type.append(previousLexeme()); + } else { + return false; + } + } else if (match(Tok_int) || match(Tok_char) || match(Tok_double)) { + type.append(previousLexeme()); + } + + matchTemplateAngles(type); + + while (match(Tok_const) || match(Tok_volatile)) + type.append(previousLexeme()); + + if (match(Tok_Gulbrandsen)) + type.append(previousLexeme()); + else + break; + } + + while (match(Tok_Ampersand) || match(Tok_Aster) || match(Tok_const) || match(Tok_Caret) + || match(Tok_Ellipsis)) + type.append(previousLexeme()); + + if (match(Tok_LeftParenAster)) { + /* + A function pointer. This would be rather hard to handle without a + tokenizer hack, because a type can be followed with a left parenthesis + in some cases (e.g., 'operator int()'). The tokenizer recognizes '(*' + as a single token. + */ + type.append(" "); // force a space after the type + type.append(previousLexeme()); + type.appendHotspot(); + if (match(Tok_Ident)) + name = previousLexeme(); + if (!match(Tok_RightParen)) + return false; + type.append(previousLexeme()); + if (!match(Tok_LeftParen)) + return false; + type.append(previousLexeme()); + + /* parse the parameters. Ignore the parameter name from the type */ + while (m_tok != Tok_RightParen && m_tok != Tok_Eoi) { + QString dummy; + if (!matchTypeAndName(type, dummy)) + return false; + if (match(Tok_Comma)) + type.append(previousLexeme()); + } + if (!match(Tok_RightParen)) + return false; + type.append(previousLexeme()); + } else { + /* + The common case: Look for an optional identifier, then for + some array brackets. + */ + type.appendHotspot(); + + if (match(Tok_Ident)) { + name = previousLexeme(); + } else if (match(Tok_Comment)) { + /* + A neat hack: Commented-out parameter names are + recognized by qdoc. It's impossible to illustrate + here inside a C-style comment, because it requires + an asterslash. It's also impossible to illustrate + inside a C++-style comment, because the explanation + does not fit on one line. + */ + auto match = s_varComment.match(previousLexeme()); + if (match.hasMatch()) + name = match.captured(1); + } else if (match(Tok_LeftParen)) { + name = "("; + while (m_tok != Tok_RightParen && m_tok != Tok_Eoi) { + name.append(lexeme()); + readToken(); + } + name.append(")"); + readToken(); + if (match(Tok_LeftBracket)) { + name.append("["); + while (m_tok != Tok_RightBracket && m_tok != Tok_Eoi) { + name.append(lexeme()); + readToken(); + } + name.append("]"); + readToken(); + } + } + + if (m_tok == Tok_LeftBracket) { + int bracketDepth0 = m_tokenizer->bracketDepth(); + while ((m_tokenizer->bracketDepth() >= bracketDepth0 && m_tok != Tok_Eoi) + || m_tok == Tok_RightBracket) { + type.append(lexeme()); + readToken(); + } + } + } + return true; +} + +/*! + Parse the next function parameter, if there is one, and + append it to the internal parameter vector. Return true + if a parameter is parsed correctly. Otherwise return false. + */ +bool Parameters::matchParameter() +{ + if (match(Tok_QPrivateSignal)) { + m_privateSignal = true; + return true; + } + + CodeChunk chunk; + QString name; + if (!matchTypeAndName(chunk, name)) + return false; + QString type = chunk.toString(); + QString defaultValue; + match(Tok_Comment); + if (match(Tok_Equal)) { + chunk.clear(); + int pdepth = m_tokenizer->parenDepth(); + while (m_tokenizer->parenDepth() >= pdepth + && (m_tok != Tok_Comma || (m_tokenizer->parenDepth() > pdepth)) + && m_tok != Tok_Eoi) { + chunk.append(lexeme()); + readToken(); + } + defaultValue = chunk.toString(); + } + append(type, name, defaultValue); + return true; +} + +/*! + This function uses a Tokenizer to parse the \a signature, + which is a comma-separated list of parameter declarations. + If an error is detected, the Parameters object is cleared + and \c false is returned. Otherwise \c true is returned. + */ +bool Parameters::parse(const QString &signature) +{ + Tokenizer *outerTokenizer = m_tokenizer; + int outerTok = m_tok; + + QByteArray latin1 = signature.toLatin1(); + Tokenizer stringTokenizer(Location(), latin1); + stringTokenizer.setParsingFnOrMacro(true); + m_tokenizer = &stringTokenizer; + + readToken(); + do { + if (!matchParameter()) { + m_parameters.clear(); + m_valid = false; + break; + } + } while (match(Tok_Comma)); + + m_tokenizer = outerTokenizer; + m_tok = outerTok; + return m_valid; +} + +/*! + Append a Parameter constructed from \a type, \a name, and \a value + to the parameter vector. + */ +void Parameters::append(const QString &type, const QString &name, const QString &value) +{ + m_parameters.append(Parameter(type, name, value)); +} + +/*! + Returns the list of reconstructed parameters. If \a includeValues + is true, the default values are included, if any are present. + */ +QString Parameters::signature(bool includeValues) const +{ + QString result; + if (!m_parameters.empty()) { + for (int i = 0; i < m_parameters.size(); i++) { + if (i > 0) + result += ", "; + result += m_parameters.at(i).signature(includeValues); + } + } + return result; +} + +/*! + Returns the signature of all the parameters with all the + spaces and commas removed. It is unintelligible, but that + is what the caller wants. + + If \a names is true, the parameter names are included. If + \a values is true, the default values are included. + */ +QString Parameters::rawSignature(bool names, bool values) const +{ + QString raw; + const auto params = m_parameters; + for (const auto ¶meter : params) { + raw += parameter.type(); + if (names) + raw += parameter.name(); + if (values) + raw += parameter.defaultValue(); + } + return raw; +} + +/*! + Parse the parameter \a signature by splitting the string, + and store the individual parameters in the parameter vector. + + This method of parsing is naive but sufficient for QML methods + and macros. + */ +void Parameters::set(const QString &signature) +{ + clear(); + if (!signature.isEmpty()) { + QStringList commaSplit = signature.split(','); + m_parameters.resize(commaSplit.size()); + int i = 0; + for (const auto &item : std::as_const(commaSplit)) { + QStringList blankSplit = item.split(' ', Qt::SkipEmptyParts); + QString pDefault; + qsizetype defaultIdx = blankSplit.indexOf(QStringLiteral("=")); + if (defaultIdx != -1) { + if (++defaultIdx < blankSplit.size()) + pDefault = blankSplit.mid(defaultIdx).join(' '); + blankSplit = blankSplit.mid(0, defaultIdx - 1); + } + QString pName = blankSplit.takeLast(); + QString pType = blankSplit.join(' '); + if (pType.isEmpty() && pName == QLatin1String("...")) + qSwap(pType, pName); + else { + int j = 0; + while (j < pName.size() && !pName.at(j).isLetter()) + j++; + if (j > 0) { + pType += QChar(' ') + pName.left(j); + pName = pName.mid(j); + } + } + m_parameters[i++].set(pType, pName, pDefault); + } + } +} + +/*! + Insert all the parameter names into names. + */ +QSet<QString> Parameters::getNames() const +{ + QSet<QString> names; + const auto params = m_parameters; + for (const auto ¶meter : params) { + if (!parameter.name().isEmpty()) + names.insert(parameter.name()); + } + return names; +} + +/*! + Construct a list of the parameter types and return it. + */ +QString Parameters::generateTypeList() const +{ + QString out; + if (count() > 0) { + for (int i = 0; i < count(); ++i) { + if (i > 0) + out += ", "; + out += m_parameters.at(i).type(); + } + } + return out; +} + +/*! + Construct a list of the parameter type/name pairs and + return it. +*/ +QString Parameters::generateTypeAndNameList() const +{ + QString out; + if (count() > 0) { + for (int i = 0; i < count(); ++i) { + if (i != 0) + out += ", "; + const Parameter &p = m_parameters.at(i); + out += p.type(); + if (out[out.size() - 1].isLetterOrNumber()) + out += QLatin1Char(' '); + out += p.name(); + } + } + return out; +} + +/*! + Returns true if \a parameters contains the same parameter + signature as this. + */ +bool Parameters::match(const Parameters ¶meters) const +{ + if (count() != parameters.count()) + return false; + if (count() == 0) + return true; + for (int i = 0; i < count(); i++) { + if (parameters.at(i).type() != m_parameters.at(i).type()) + return false; + } + return true; +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/parameters.h b/src/qdoc/qdoc/src/qdoc/parameters.h new file mode 100644 index 000000000..1417b0958 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/parameters.h @@ -0,0 +1,113 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef PARAMETERS_H +#define PARAMETERS_H + +#include <QtCore/qlist.h> +#include <QtCore/qregularexpression.h> +#include <QtCore/qset.h> + +#include <utility> + +QT_BEGIN_NAMESPACE + +class Location; +class Tokenizer; +class CodeChunk; + +class Parameter +{ +public: + Parameter() = default; + explicit Parameter(QString type, QString name = QString(), QString defaultValue = QString()) + : m_type(std::move(type)), m_name(std::move(name)), m_defaultValue(std::move(defaultValue)) + { + } + + void setName(const QString &name) { m_name = name; } + [[nodiscard]] bool hasType() const { return !m_type.isEmpty(); } + [[nodiscard]] const QString &type() const { return m_type; } + [[nodiscard]] const QString &name() const { return m_name; } + [[nodiscard]] const QString &defaultValue() const { return m_defaultValue; } + void setDefaultValue(const QString &t) { m_defaultValue = t; } + + void set(const QString &type, const QString &name, const QString &defaultValue = QString()) + { + m_type = type; + m_name = name; + m_defaultValue = defaultValue; + } + + [[nodiscard]] QString signature(bool includeValue = false) const; + + [[nodiscard]] const QString &canonicalType() const { return m_canonicalType; } + void setCanonicalType(const QString &t) { m_canonicalType = t; } + +public: + QString m_canonicalType {}; + QString m_type {}; + QString m_name {}; + QString m_defaultValue {}; +}; + +typedef QList<Parameter> ParameterVector; + +class Parameters +{ +public: + Parameters(); + Parameters(const QString &signature); // TODO: Making this explicit breaks QDoc + + void clear() + { + m_parameters.clear(); + m_privateSignal = false; + m_valid = true; + } + [[nodiscard]] const ParameterVector ¶meters() const { return m_parameters; } + [[nodiscard]] bool isPrivateSignal() const { return m_privateSignal; } + [[nodiscard]] bool isEmpty() const { return m_parameters.isEmpty(); } + [[nodiscard]] bool isValid() const { return m_valid; } + [[nodiscard]] int count() const { return m_parameters.size(); } + void reserve(int count) { m_parameters.reserve(count); } + [[nodiscard]] const Parameter &at(int i) const { return m_parameters.at(i); } + Parameter &last() { return m_parameters.last(); } + [[nodiscard]] const Parameter &last() const { return m_parameters.last(); } + inline Parameter &operator[](int index) { return m_parameters[index]; } + void append(const QString &type, const QString &name, const QString &value); + void append(const QString &type, const QString &name) { append(type, name, QString()); } + void append(const QString &type) { append(type, QString(), QString()); } + void pop_back() { m_parameters.pop_back(); } + void setPrivateSignal() { m_privateSignal = true; } + [[nodiscard]] QString signature(bool includeValues = false) const; + [[nodiscard]] QString rawSignature(bool names = false, bool values = false) const; + void set(const QString &signature); + [[nodiscard]] QSet<QString> getNames() const; + [[nodiscard]] QString generateTypeList() const; + [[nodiscard]] QString generateTypeAndNameList() const; + [[nodiscard]] bool match(const Parameters ¶meters) const; + +private: + void readToken(); + QString lexeme(); + QString previousLexeme(); + bool match(int target); + void matchTemplateAngles(CodeChunk &type); + bool matchTypeAndName(CodeChunk &type, QString &name); + bool matchParameter(); + bool parse(const QString &signature); + +private: + static QRegularExpression s_varComment; + + bool m_valid {}; + bool m_privateSignal {}; + int m_tok {}; + Tokenizer *m_tokenizer { nullptr }; + ParameterVector m_parameters; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/parsererror.cpp b/src/qdoc/qdoc/src/qdoc/parsererror.cpp new file mode 100644 index 000000000..b55660572 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/parsererror.cpp @@ -0,0 +1,90 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "parsererror.h" +#include "node.h" +#include "qdocdatabase.h" +#include "config.h" +#include "utilities.h" + +#include <QtCore/qregularexpression.h> + +using namespace Qt::Literals::StringLiterals; + +QT_BEGIN_NAMESPACE + +/*! + \class FnMatchError + \brief Encapsulates information about \fn match error during parsing. +*/ + +/*! + \variable FnMatchError::signature + + Signature for the \fn topic that failed to match. +*/ + +/*! + \relates FnMatchError + + Returns \c true if any parent of a C++ function represented by + \a signature is documented as \\internal. +*/ +bool isParentInternal(const QString &signature) +{ + const QRegularExpression scoped_fn{R"((?:\w+(?:<[^>]+>)?::)+~?\w\S*\()"}; + auto match = scoped_fn.match(signature); + if (!match.isValid()) + return false; + + auto scope = match.captured().split("::"_L1); + scope.removeLast(); // Drop function name + + for (auto &s : scope) + if (qsizetype pos = s.indexOf('<'); pos >= 0) + s.truncate(pos); + + auto parent = QDocDatabase::qdocDB()->findNodeByNameAndType(scope, &Node::isCppNode); + if (parent && !(parent->isClassNode() || parent->isNamespace())) { + qCDebug(lcQdoc).noquote() + << "Invalid scope:" << qPrintable(parent->nodeTypeString()) + << qPrintable(parent->fullName()) + << "for \\fn" << qPrintable(signature); + return false; + } + + while (parent) { + if (parent->isInternal()) + return true; + parent = parent->parent(); + } + + return false; +} + +/*! + \class ParserErrorHandler + \brief Processes parser errors and outputs warnings for them. +*/ + +/*! + Generates a warning specific to FnMatchError. + + Warnings for internal documentation are omitted. Specifically, this + (omission) happens if: + + \list + \li \c {--showinternal} command line option is \b not + used, and + \li The warning is for an \\fn that is declared + under a namespace/class that is documented as + \\internal. + \endlist +*/ +void ParserErrorHandler::operator()(const FnMatchError &e) const +{ + if (Config::instance().showInternal() || !isParentInternal(e.signature)) + e.location.warning("Failed to find function when parsing \\fn %1"_L1.arg(e.signature)); +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/parsererror.h b/src/qdoc/qdoc/src/qdoc/parsererror.h new file mode 100644 index 000000000..85435366f --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/parsererror.h @@ -0,0 +1,26 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef PARSERERROR_H +#define PARSERERROR_H + +#include "location.h" + +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +struct FnMatchError { + QString signature {}; + Location location {}; + +}; + +struct ParserErrorHandler +{ + void operator()(const FnMatchError &e) const; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/propertynode.cpp b/src/qdoc/qdoc/src/qdoc/propertynode.cpp new file mode 100644 index 000000000..6607af5bd --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/propertynode.cpp @@ -0,0 +1,135 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "propertynode.h" + +#include "aggregate.h" + +QT_BEGIN_NAMESPACE + +/*! + \class PropertyNode + + This class describes one instance of using the Q_PROPERTY macro. + */ + +/*! + The constructor sets the \a parent and the \a name, but + everything else is left to default values. + */ +PropertyNode::PropertyNode(Aggregate *parent, const QString &name) : Node(Property, parent, name) +{ + // nothing +} + + +/*! + Returns a string representing an access function \a role. +*/ +QString PropertyNode::roleName(FunctionRole role) +{ + switch (role) { + case FunctionRole::Getter: + return "getter"; + case FunctionRole::Setter: + return "setter"; + case FunctionRole::Resetter: + return "resetter"; + case FunctionRole::Notifier: + return "notifier"; + case FunctionRole::Bindable: + return "bindable"; + default: + break; + } + return QString(); +} + +/*! + Sets this property's \e {overridden from} property to + \a baseProperty, which indicates that this property + overrides \a baseProperty. To begin with, all the values + in this property are set to the corresponding values in + \a baseProperty. + + We probably should ensure that the constant and final + attributes are not being overridden improperly. + */ +void PropertyNode::setOverriddenFrom(const PropertyNode *baseProperty) +{ + for (qsizetype i{0}; i < (qsizetype)FunctionRole::NumFunctionRoles; ++i) { + if (m_functions[i].isEmpty()) + m_functions[i] = baseProperty->m_functions[i]; + } + if (m_stored == FlagValueDefault) + m_stored = baseProperty->m_stored; + if (m_writable == FlagValueDefault) + m_writable = baseProperty->m_writable; + if (m_user == FlagValueDefault) + m_user = baseProperty->m_user; + m_overrides = baseProperty; +} + +/*! + Returns a string containing the data type qualified with "const" either + prepended to the data type or appended to it, or without the const + qualification, depending circumstances in the PropertyNode internal state. + */ +QString PropertyNode::qualifiedDataType() const +{ + if (m_propertyType != PropertyType::StandardProperty || m_type.startsWith(QLatin1String("const "))) + return m_type; + + if (setters().isEmpty() && resetters().isEmpty()) { + if (m_type.contains(QLatin1Char('*')) || m_type.contains(QLatin1Char('&'))) { + // 'QWidget *' becomes 'QWidget *' const + return m_type + " const"; + } else { + /* + 'int' becomes 'const int' ('int const' is + correct C++, but looks wrong) + */ + return "const " + m_type; + } + } else { + return m_type; + } +} + +/*! + Returns true if this property has an access function named \a name. + */ +bool PropertyNode::hasAccessFunction(const QString &name) const +{ + for (const auto &getter : getters()) { + if (getter->name() == name) + return true; + } + for (const auto &setter : setters()) { + if (setter->name() == name) + return true; + } + for (const auto &resetter : resetters()) { + if (resetter->name() == name) + return true; + } + for (const auto ¬ifier : notifiers()) { + if (notifier->name() == name) + return true; + } + return false; +} + +/*! + Returns the role of \a functionNode for this property. + */ +PropertyNode::FunctionRole PropertyNode::role(const FunctionNode *functionNode) const +{ + for (qsizetype i{0}; i < (qsizetype)FunctionRole::NumFunctionRoles; i++) { + if (m_functions[i].contains(const_cast<FunctionNode *>(functionNode))) + return (FunctionRole)i; + } + return FunctionRole::Notifier; // TODO: Figure out a better way to handle 'not found'. +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/propertynode.h b/src/qdoc/qdoc/src/qdoc/propertynode.h new file mode 100644 index 000000000..9ae59932b --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/propertynode.h @@ -0,0 +1,93 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef PROPERTYNODE_H +#define PROPERTYNODE_H + +#include "functionnode.h" +#include "node.h" + +#include <QtCore/qglobal.h> +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +class Aggregate; + +class PropertyNode : public Node +{ +public: + enum class PropertyType { StandardProperty, BindableProperty }; + enum class FunctionRole { Getter, Setter, Resetter, Notifier, Bindable, NumFunctionRoles }; + static QString roleName(FunctionRole role); + + PropertyNode(Aggregate *parent, const QString &name); + + void setDataType(const QString &dataType) override { m_type = dataType; } + void addFunction(FunctionNode *function, FunctionRole role); + void addSignal(FunctionNode *function, FunctionRole role); + void setStored(bool stored) { m_stored = toFlagValue(stored); } + void setWritable(bool writable) { m_writable = toFlagValue(writable); } + void setOverriddenFrom(const PropertyNode *baseProperty); + void setConstant() { m_const = true; } + void setRequired() { m_required = true; } + void setPropertyType(PropertyType type) { m_propertyType = type; } + + [[nodiscard]] const QString &dataType() const { return m_type; } + [[nodiscard]] QString qualifiedDataType() const; + [[nodiscard]] NodeList functions() const; + [[nodiscard]] const NodeList &functions(FunctionRole role) const + { + return m_functions[(int)role]; + } + [[nodiscard]] const NodeList &getters() const { return functions(FunctionRole::Getter); } + [[nodiscard]] const NodeList &setters() const { return functions(FunctionRole::Setter); } + [[nodiscard]] const NodeList &resetters() const { return functions(FunctionRole::Resetter); } + [[nodiscard]] const NodeList ¬ifiers() const { return functions(FunctionRole::Notifier); } + [[nodiscard]] bool hasAccessFunction(const QString &name) const; + FunctionRole role(const FunctionNode *functionNode) const; + [[nodiscard]] bool isStored() const { return fromFlagValue(m_stored, storedDefault()); } + [[nodiscard]] bool isWritable() const { return fromFlagValue(m_writable, writableDefault()); } + [[nodiscard]] bool isConstant() const { return m_const; } + [[nodiscard]] bool isRequired() const { return m_required; } + [[nodiscard]] PropertyType propertyType() const { return m_propertyType; } + [[nodiscard]] const PropertyNode *overriddenFrom() const { return m_overrides; } + + [[nodiscard]] bool storedDefault() const { return true; } + [[nodiscard]] bool writableDefault() const { return !setters().isEmpty(); } + +private: + QString m_type {}; + PropertyType m_propertyType { PropertyType::StandardProperty }; + NodeList m_functions[(qsizetype)FunctionRole::NumFunctionRoles] {}; + FlagValue m_stored { FlagValueDefault }; + FlagValue m_writable { FlagValueDefault }; + FlagValue m_user { FlagValueDefault }; + bool m_const { false }; + bool m_required { false }; + const PropertyNode *m_overrides { nullptr }; +}; + +inline void PropertyNode::addFunction(FunctionNode *function, FunctionRole role) +{ + m_functions[(int)role].append(function); + function->addAssociatedProperty(this); +} + +inline void PropertyNode::addSignal(FunctionNode *function, FunctionRole role) +{ + m_functions[(int)role].append(function); + function->addAssociatedProperty(this); +} + +inline NodeList PropertyNode::functions() const +{ + NodeList list; + for (qsizetype i{0}; i < (qsizetype)FunctionRole::NumFunctionRoles; ++i) + list += m_functions[i]; + return list; +} + +QT_END_NAMESPACE + +#endif // PROPERTYNODE_H diff --git a/src/qdoc/qdoc/src/qdoc/proxynode.cpp b/src/qdoc/qdoc/src/qdoc/proxynode.cpp new file mode 100644 index 000000000..49e4be34e --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/proxynode.cpp @@ -0,0 +1,54 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "proxynode.h" + +#include "tree.h" + +QT_BEGIN_NAMESPACE + +/*! + \class ProxyNode + \brief A class for representing an Aggregate that is documented in a different module. + + This class is used to represent an Aggregate (usually a class) + that is located and documented in a different module. In the + current module, a ProxyNode holds child nodes that are related + to the class in the other module. + + For example, class QHash is located and documented in QtCore. + There are many global functions named qHash() in QtCore that + are all related to class QHash using the \c relates command. + There are also a few qHash() function in QtNetwork that are + related to QHash. These functions must be documented when the + documentation for QtNetwork is generated, but the reference + page for QHash must link to that documentation in its related + nonmembers list. + + The ProxyNode allows qdoc to construct links to the related + functions (or other things?) in QtNetwork from the reference + page in QtCore. + */ + +/*! + Constructs the ProxyNode, which at this point looks like any + other Aggregate, and then finds the Tree this node is in and + appends this node to that Tree's proxy list so it will be + easy to find later. + */ +ProxyNode::ProxyNode(Aggregate *parent, const QString &name) : Aggregate(Node::Proxy, parent, name) +{ + tree()->appendProxy(this); +} + +/*! \fn bool ProxyNode::docMustBeGenerated() const + Returns true because a ProxyNode always means some documentation + must be generated. +*/ + +/*! \fn bool ProxyNode::isRelatableType() const + Returns true because the ProxyNode exists so that elements + can be related to it with the \c {\\relates} command. +*/ + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/proxynode.h b/src/qdoc/qdoc/src/qdoc/proxynode.h new file mode 100644 index 000000000..cced34892 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/proxynode.h @@ -0,0 +1,23 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef PROXYNODE_H +#define PROXYNODE_H + +#include "aggregate.h" + +#include <QtCore/qglobal.h> + +QT_BEGIN_NAMESPACE + +class ProxyNode : public Aggregate +{ +public: + ProxyNode(Aggregate *parent, const QString &name); + [[nodiscard]] bool docMustBeGenerated() const override { return true; } + [[nodiscard]] bool isRelatableType() const override { return true; } +}; + +QT_END_NAMESPACE + +#endif // PROXYNODE_H diff --git a/src/qdoc/qdoc/src/qdoc/puredocparser.cpp b/src/qdoc/qdoc/src/qdoc/puredocparser.cpp new file mode 100644 index 000000000..c6de06a9c --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/puredocparser.cpp @@ -0,0 +1,74 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "puredocparser.h" + +#include "qdocdatabase.h" +#include "tokenizer.h" + +#include <cerrno> + +QT_BEGIN_NAMESPACE + +/*! + Parses the source file identified by \a filePath and adds its + parsed contents to the database. The \a location is used for + reporting errors. + */ +std::vector<UntiedDocumentation> PureDocParser::parse_qdoc_file(const QString &filePath) +{ + QFile in(filePath); + if (!in.open(QIODevice::ReadOnly)) { + location.error( + QStringLiteral("Can't open source file '%1' (%2)").arg(filePath, strerror(errno))); + return {}; + } + + return processQdocComments(in); +} + +/*! + This is called by parseSourceFile() to do the actual parsing + and tree building. It only processes qdoc comments. It skips + everything else. + */ +std::vector<UntiedDocumentation> PureDocParser::processQdocComments(QFile& input_file) +{ + std::vector<UntiedDocumentation> untied{}; + + Tokenizer tokenizer(Location{input_file.fileName()}, input_file); + + const QSet<QString> &commands = CppCodeParser::topic_commands + CppCodeParser::meta_commands; + + int token = tokenizer.getToken(); + while (token != Tok_Eoi) { + if (token != Tok_Doc) { + token = tokenizer.getToken(); + continue; + } + QString comment = tokenizer.lexeme(); // returns an entire qdoc comment. + Location start_loc(tokenizer.location()); + token = tokenizer.getToken(); + + Doc::trimCStyleComment(start_loc, comment); + Location end_loc(tokenizer.location()); + + // Doc constructor parses the comment. + Doc doc(start_loc, end_loc, comment, commands, CppCodeParser::topic_commands); + if (doc.topicsUsed().isEmpty()) { + doc.location().warning(QStringLiteral("This qdoc comment contains no topic command " + "(e.g., '\\%1', '\\%2').") + .arg(COMMAND_MODULE, COMMAND_PAGE)); + continue; + } + + if (hasTooManyTopics(doc)) + continue; + + untied.emplace_back(UntiedDocumentation{doc, QStringList()}); + } + + return untied; +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/puredocparser.h b/src/qdoc/qdoc/src/qdoc/puredocparser.h new file mode 100644 index 000000000..d3467ed92 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/puredocparser.h @@ -0,0 +1,31 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef PUREDOCPARSER_H +#define PUREDOCPARSER_H + +#include "cppcodeparser.h" + +#include <QtCore/QFile> + +QT_BEGIN_NAMESPACE + +class Location; + +class PureDocParser +{ +public: + PureDocParser(const Location& location) : location{location} {} + + std::vector<UntiedDocumentation> parse_qdoc_file(const QString& filePath); + +private: + std::vector<UntiedDocumentation> processQdocComments(QFile& input_file); + +private: + const Location& location; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/qdoccommandlineparser.cpp b/src/qdoc/qdoc/src/qdoc/qdoccommandlineparser.cpp new file mode 100644 index 000000000..6586056a4 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/qdoccommandlineparser.cpp @@ -0,0 +1,177 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "qdoccommandlineparser.h" + +#include "utilities.h" + +#include <QtCore/qdebug.h> +#include <QtCore/qfile.h> + +QDocCommandLineParser::QDocCommandLineParser() + : QCommandLineParser(), + defineOption(QStringList() << QStringLiteral("D")), + dependsOption(QStringList() << QStringLiteral("depends")), + highlightingOption(QStringList() << QStringLiteral("highlighting")), + showInternalOption(QStringList() << QStringLiteral("showinternal")), + redirectDocumentationToDevNullOption(QStringList() + << QStringLiteral("redirect-documentation-to-dev-null")), + noExamplesOption(QStringList() << QStringLiteral("no-examples")), + indexDirOption(QStringList() << QStringLiteral("indexdir")), + installDirOption(QStringList() << QStringLiteral("installdir")), + outputDirOption(QStringList() << QStringLiteral("outputdir")), + outputFormatOption(QStringList() << QStringLiteral("outputformat")), + noLinkErrorsOption(QStringList() << QStringLiteral("no-link-errors")), + autoLinkErrorsOption(QStringList() << QStringLiteral("autolink-errors")), + debugOption(QStringList() << QStringLiteral("debug")), + atomsDumpOption("atoms-dump"), + prepareOption(QStringList() << QStringLiteral("prepare")), + generateOption(QStringList() << QStringLiteral("generate")), + logProgressOption(QStringList() << QStringLiteral("log-progress")), + singleExecOption(QStringList() << QStringLiteral("single-exec")), + includePathOption("I", "Add dir to the include path for header files.", "path"), + includePathSystemOption("isystem", "Add dir to the system include path for header files.", + "path"), + frameworkOption("F", "Add macOS framework to the include path for header files.", + "framework"), + timestampsOption(QStringList() << QStringLiteral("timestamps")), + useDocBookExtensions(QStringList() << QStringLiteral("docbook-extensions")) +{ + setApplicationDescription(QStringLiteral("Qt documentation generator")); + addHelpOption(); + addVersionOption(); + + setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions); + + addPositionalArgument("file1.qdocconf ...", QStringLiteral("Input files")); + + defineOption.setDescription( + QStringLiteral("Define the argument as a macro while parsing sources")); + defineOption.setValueName(QStringLiteral("macro[=def]")); + addOption(defineOption); + + dependsOption.setDescription(QStringLiteral("Specify dependent modules")); + dependsOption.setValueName(QStringLiteral("module")); + addOption(dependsOption); + + highlightingOption.setDescription( + QStringLiteral("Turn on syntax highlighting (makes qdoc run slower)")); + addOption(highlightingOption); + + showInternalOption.setDescription(QStringLiteral("Include content marked internal")); + addOption(showInternalOption); + + redirectDocumentationToDevNullOption.setDescription( + QStringLiteral("Save all documentation content to /dev/null. " + " Useful if someone is interested in qdoc errors only.")); + addOption(redirectDocumentationToDevNullOption); + + noExamplesOption.setDescription(QStringLiteral("Do not generate documentation for examples")); + addOption(noExamplesOption); + + indexDirOption.setDescription( + QStringLiteral("Specify a directory where QDoc should search for index files to load")); + indexDirOption.setValueName(QStringLiteral("dir")); + addOption(indexDirOption); + + installDirOption.setDescription(QStringLiteral( + "Specify the directory where the output will be after running \"make install\"")); + installDirOption.setValueName(QStringLiteral("dir")); + addOption(installDirOption); + + outputDirOption.setDescription( + QStringLiteral("Specify output directory, overrides setting in qdocconf file")); + outputDirOption.setValueName(QStringLiteral("dir")); + addOption(outputDirOption); + + outputFormatOption.setDescription( + QStringLiteral("Specify output format, overrides setting in qdocconf file")); + outputFormatOption.setValueName(QStringLiteral("format")); + addOption(outputFormatOption); + + noLinkErrorsOption.setDescription( + QStringLiteral("Do not print link errors (i.e. missing targets)")); + addOption(noLinkErrorsOption); + + autoLinkErrorsOption.setDescription(QStringLiteral("Show errors when automatic linking fails")); + addOption(autoLinkErrorsOption); + + debugOption.setDescription(QStringLiteral("Enable debug output")); + addOption(debugOption); + + atomsDumpOption.setDescription(QStringLiteral( + "Shows a human-readable form of the intermediate result of parsing a block-comment.")); + addOption(atomsDumpOption); + + prepareOption.setDescription( + QStringLiteral("Run qdoc only to generate an index file, not the docs")); + addOption(prepareOption); + + generateOption.setDescription( + QStringLiteral("Run qdoc to read the index files and generate the docs")); + addOption(generateOption); + + logProgressOption.setDescription(QStringLiteral("Log progress on stderr.")); + addOption(logProgressOption); + + singleExecOption.setDescription(QStringLiteral("Run qdoc once over all the qdoc conf files.")); + addOption(singleExecOption); + + includePathOption.setFlags(QCommandLineOption::ShortOptionStyle); + addOption(includePathOption); + + addOption(includePathSystemOption); + + frameworkOption.setFlags(QCommandLineOption::ShortOptionStyle); + addOption(frameworkOption); + + timestampsOption.setDescription(QStringLiteral("Timestamp each qdoc log line.")); + addOption(timestampsOption); + + useDocBookExtensions.setDescription( + QStringLiteral("Use the DocBook Library extensions for metadata.")); + addOption(useDocBookExtensions); +} + +/*! + * \internal + * + * Create a list of arguments from the command line and/or file(s). + * This lets QDoc accept arguments contained in a file provided as a + * command-line argument prepended by '@'. + */ +static QStringList argumentsFromCommandLineAndFile(const QStringList &arguments) +{ + QStringList allArguments; + allArguments.reserve(arguments.size()); + for (const QString &argument : arguments) { + // "@file" doesn't start with a '-' so we can't use QCommandLineParser for it + if (argument.startsWith(QLatin1Char('@'))) { + QString optionsFile = argument; + optionsFile.remove(0, 1); + if (optionsFile.isEmpty()) + qFatal("The @ option requires an input file"); + QFile f(optionsFile); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) + qFatal("Cannot open options file specified with @: %ls", + qUtf16Printable(optionsFile)); + while (!f.atEnd()) { + QString line = QString::fromLocal8Bit(f.readLine().trimmed()); + if (!line.isEmpty()) + allArguments << line; + } + } else { + allArguments << argument; + } + } + return allArguments; +} + +void QDocCommandLineParser::process(const QStringList &arguments) +{ + auto allArguments = argumentsFromCommandLineAndFile(arguments); + QCommandLineParser::process(allArguments); + + if (isSet(singleExecOption) && isSet(indexDirOption)) + qCWarning(lcQdoc) << "Warning: -indexdir option ignored: Index files are not used in single-exec mode."; +} diff --git a/src/qdoc/qdoc/src/qdoc/qdoccommandlineparser.h b/src/qdoc/qdoc/src/qdoc/qdoccommandlineparser.h new file mode 100644 index 000000000..57b36b582 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/qdoccommandlineparser.h @@ -0,0 +1,28 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef QDOCCOMMANDLINEPARSER_H +#define QDOCCOMMANDLINEPARSER_H + +#include <QtCore/qcommandlineparser.h> + +QT_BEGIN_NAMESPACE + +struct QDocCommandLineParser : public QCommandLineParser +{ + QDocCommandLineParser(); + void process(const QStringList &arguments); + + QCommandLineOption defineOption, dependsOption, highlightingOption; + QCommandLineOption showInternalOption, redirectDocumentationToDevNullOption; + QCommandLineOption noExamplesOption, indexDirOption, installDirOption; + QCommandLineOption outputDirOption, outputFormatOption; + QCommandLineOption noLinkErrorsOption, autoLinkErrorsOption, debugOption, atomsDumpOption; + QCommandLineOption prepareOption, generateOption, logProgressOption, singleExecOption; + QCommandLineOption includePathOption, includePathSystemOption, frameworkOption; + QCommandLineOption timestampsOption, useDocBookExtensions; +}; + +QT_END_NAMESPACE + +#endif // QDOCCOMMANDLINEPARSER_H diff --git a/src/qdoc/qdoc/src/qdoc/qdocdatabase.cpp b/src/qdoc/qdoc/src/qdoc/qdocdatabase.cpp new file mode 100644 index 000000000..57e88fbde --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/qdocdatabase.cpp @@ -0,0 +1,1685 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "qdocdatabase.h" + +#include "atom.h" +#include "collectionnode.h" +#include "functionnode.h" +#include "generator.h" +#include "qdocindexfiles.h" +#include "tree.h" + +#include <QtCore/qregularexpression.h> +#include <stack> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; +static NodeMultiMap emptyNodeMultiMap_; + +/*! + \class QDocForest + + A class representing a forest of Tree objects. + + This private class manages a collection of Tree objects (a + forest) for the singleton QDocDatabase object. It is only + accessed by that singleton QDocDatabase object, which is a + friend. Each tree in the forest is an instance of class + Tree, which is a mostly private class. Both QDocForest and + QDocDatabase are friends of Tree and have full access. + + There are two kinds of trees in the forest, differing not + in structure but in use. One Tree is the primary tree. It + is the tree representing the module being documented. All + the other trees in the forest are called index trees. Each + one represents the contents of the index file for one of + the modules the current module must be able to link to. + + The instances of subclasses of Node in the primary tree + will contain documentation in an instance of Doc. The + index trees contain no documentation, and each Node in + an index tree is marked as an index node. + + Each tree is named with the name of its module. + + The search order is created by searchOrder(), if it has + not already been created. The search order and module + names arrays have parallel structure, i.e. modulNames_[i] + is the module name of the Tree at searchOrder_[i]. + + The primary tree is always the first tree in the search + order. i.e., when the database is searched, the primary + tree is always searched first, unless a specific tree is + being searched. + */ + +/*! + Destroys the qdoc forest. This requires deleting + each Tree in the forest. Note that the forest has + been transferred into the search order array, so + what is really being used to destroy the forest + is the search order array. + */ +QDocForest::~QDocForest() +{ + for (auto *entry : m_searchOrder) + delete entry; + m_forest.clear(); + m_searchOrder.clear(); + m_indexSearchOrder.clear(); + m_moduleNames.clear(); + m_primaryTree = nullptr; +} + +/*! + Initializes the forest prior to a traversal and + returns a pointer to the primary tree. If the + forest is empty, it returns \nullptr. + */ +Tree *QDocForest::firstTree() +{ + m_currentIndex = 0; + return (!searchOrder().isEmpty() ? searchOrder()[0] : nullptr); +} + +/*! + Increments the forest's current tree index. If the current + tree index is still within the forest, the function returns + the pointer to the current tree. Otherwise it returns \nullptr. + */ +Tree *QDocForest::nextTree() +{ + ++m_currentIndex; + return (m_currentIndex < searchOrder().size() ? searchOrder()[m_currentIndex] : nullptr); +} + +/*! + \fn Tree *QDocForest::primaryTree() + + Returns the pointer to the primary tree. + */ + +/*! + Finds the tree for module \a t in the forest and + sets the primary tree to be that tree. After the + primary tree is set, that tree is removed from the + forest. + + \node It gets re-inserted into the forest after the + search order is built. + */ +void QDocForest::setPrimaryTree(const QString &t) +{ + QString T = t.toLower(); + m_primaryTree = findTree(T); + m_forest.remove(T); + if (m_primaryTree == nullptr) + qCCritical(lcQdoc) << "Error: Could not set primary tree to" << t; +} + +/*! + If the search order array is empty, create the search order. + If the search order array is not empty, do nothing. + */ +void QDocForest::setSearchOrder(const QStringList &t) +{ + if (!m_searchOrder.isEmpty()) + return; + + /* Allocate space for the search order. */ + m_searchOrder.reserve(m_forest.size() + 1); + m_searchOrder.clear(); + m_moduleNames.reserve(m_forest.size() + 1); + m_moduleNames.clear(); + + /* The primary tree is always first in the search order. */ + QString primaryName = primaryTree()->physicalModuleName(); + m_searchOrder.append(m_primaryTree); + m_moduleNames.append(primaryName); + m_forest.remove(primaryName); + + for (const QString &m : t) { + if (primaryName != m) { + auto it = m_forest.find(m); + if (it != m_forest.end()) { + m_searchOrder.append(it.value()); + m_moduleNames.append(m); + m_forest.remove(m); + } + } + } + /* + If any trees remain in the forest, just add them + to the search order sequentially, because we don't + know any better at this point. + */ + if (!m_forest.isEmpty()) { + for (auto it = m_forest.begin(); it != m_forest.end(); ++it) { + m_searchOrder.append(it.value()); + m_moduleNames.append(it.key()); + } + m_forest.clear(); + } + + /* + Rebuild the forest after constructing the search order. + It was destroyed during construction of the search order, + but it is needed for module-specific searches. + + Note that this loop also inserts the primary tree into the + forrest. That is a requirement. + */ + for (int i = 0; i < m_searchOrder.size(); ++i) { + if (!m_forest.contains(m_moduleNames.at(i))) { + m_forest.insert(m_moduleNames.at(i), m_searchOrder.at(i)); + } + } +} + +/*! + Returns an ordered array of Tree pointers that represents + the order in which the trees should be searched. The first + Tree in the array is the tree for the current module, i.e. + the module for which qdoc is generating documentation. + + The other Tree pointers in the array represent the index + files that were loaded in preparation for generating this + module's documentation. Each Tree pointer represents one + index file. The index file Tree points have been ordered + heuristically to, hopefully, minimize searching. Thr order + will probably be changed. + + If the search order array is empty, this function calls + indexSearchOrder(). The search order array is empty while + the index files are being loaded, but some searches must + be performed during this time, notably searches for base + class nodes. These searches require a temporary search + order. The temporary order changes throughout the loading + of the index files, but it is always the tree for the + current index file first, followed by the trees for the + index files that have already been loaded. The only + ordering required in this temporary search order is that + the current tree must be searched first. + */ +const QList<Tree *> &QDocForest::searchOrder() +{ + if (m_searchOrder.isEmpty()) + return indexSearchOrder(); + return m_searchOrder; +} + +/*! + There are two search orders used by qdoc when searching for + things. The normal search order is returned by searchOrder(), + but this normal search order is not known until all the index + files have been read. At that point, setSearchOrder() is + called. + + During the reading of the index files, the vector holding + the normal search order remains empty. Whenever the search + order is requested, if that vector is empty, this function + is called to return a temporary search order, which includes + all the index files that have been read so far, plus the + one being read now. That one is prepended to the front of + the vector. + */ +const QList<Tree *> &QDocForest::indexSearchOrder() +{ + if (m_forest.size() > m_indexSearchOrder.size()) + m_indexSearchOrder.prepend(m_primaryTree); + return m_indexSearchOrder; +} + +/*! + Create a new Tree for the index file for the specified + \a module and add it to the forest. Return the pointer + to its root. + */ +NamespaceNode *QDocForest::newIndexTree(const QString &module) +{ + m_primaryTree = new Tree(module, m_qdb); + m_forest.insert(module.toLower(), m_primaryTree); + return m_primaryTree->root(); +} + +/*! + Create a new Tree for use as the primary tree. This tree + will represent the primary module. \a module is camel case. + */ +void QDocForest::newPrimaryTree(const QString &module) +{ + m_primaryTree = new Tree(module, m_qdb); +} + +/*! + Searches through the forest for a node named \a targetPath + and returns a pointer to it if found. The \a relative node + is the starting point. It only makes sense for the primary + tree, which is searched first. After the primary tree has + been searched, \a relative is set to 0 for searching the + other trees, which are all index trees. With relative set + to 0, the starting point for each index tree is the root + of the index tree. + + If \a targetPath is resolved successfully but it refers to + a \\section title, continue the search, keeping the section + title as a fallback if no higher-priority targets are found. + */ +const Node *QDocForest::findNodeForTarget(QStringList &targetPath, const Node *relative, + Node::Genus genus, QString &ref) +{ + int flags = SearchBaseClasses | SearchEnumValues; + + QString entity = targetPath.takeFirst(); + QStringList entityPath = entity.split("::"); + + QString target; + if (!targetPath.isEmpty()) + target = targetPath.takeFirst(); + + TargetRec::TargetType type = TargetRec::Unknown; + const Node *tocNode = nullptr; + for (const auto *tree : searchOrder()) { + const Node *n = tree->findNodeForTarget(entityPath, target, relative, flags, genus, ref, &type); + if (n) { + // Targets referring to non-section titles are returned immediately + if (type != TargetRec::Contents) + return n; + if (!tocNode) + tocNode = n; + } + relative = nullptr; + } + return tocNode; +} + +/*! + Finds the FunctionNode for the qualified function name + in \a path, that also has the specified \a parameters. + Returns a pointer to the first matching function. + + \a relative is a node in the primary tree where the search + should begin. It is only used when searching the primary + tree. \a genus can be used to force the search to find a + C++ function or a QML function. + */ +const FunctionNode *QDocForest::findFunctionNode(const QStringList &path, + const Parameters ¶meters, const Node *relative, + Node::Genus genus) +{ + for (const auto *tree : searchOrder()) { + const FunctionNode *fn = tree->findFunctionNode(path, parameters, relative, genus); + if (fn) + return fn; + relative = nullptr; + } + return nullptr; +} + +/*! \class QDocDatabase + This class provides exclusive access to the qdoc database, + which consists of a forrest of trees and a lot of maps and + other useful data structures. + */ + +QDocDatabase *QDocDatabase::s_qdocDB = nullptr; +NodeMap QDocDatabase::s_typeNodeMap; +NodeMultiMap QDocDatabase::s_obsoleteClasses; +NodeMultiMap QDocDatabase::s_classesWithObsoleteMembers; +NodeMultiMap QDocDatabase::s_obsoleteQmlTypes; +NodeMultiMap QDocDatabase::s_qmlTypesWithObsoleteMembers; +NodeMultiMap QDocDatabase::s_cppClasses; +NodeMultiMap QDocDatabase::s_qmlBasicTypes; +NodeMultiMap QDocDatabase::s_qmlTypes; +NodeMultiMap QDocDatabase::s_examples; +NodeMultiMapMap QDocDatabase::s_newClassMaps; +NodeMultiMapMap QDocDatabase::s_newQmlTypeMaps; +NodeMultiMapMap QDocDatabase::s_newEnumValueMaps; +NodeMultiMapMap QDocDatabase::s_newSinceMaps; + +/*! + Constructs the singleton qdoc database object. The singleton + constructs the \a forest_ object, which is also a singleton. + \a m_showInternal is normally false. If it is true, qdoc will + write documentation for nodes marked \c internal. + + \a singleExec_ is false when qdoc is being used in the standard + way of running qdoc twices for each module, first with -prepare + and then with -generate. First the -prepare phase is run for + each module, then the -generate phase is run for each module. + + When \a singleExec_ is true, qdoc is run only once. During the + single execution, qdoc processes the qdocconf files for all the + modules sequentially in a loop. Each source file for each module + is read exactly once. + */ +QDocDatabase::QDocDatabase() : m_forest(this) +{ + // nothing +} + +/*! + Creates the singleton. Allows only one instance of the class + to be created. Returns a pointer to the singleton. +*/ +QDocDatabase *QDocDatabase::qdocDB() +{ + if (s_qdocDB == nullptr) { + s_qdocDB = new QDocDatabase; + initializeDB(); + } + return s_qdocDB; +} + +/*! + Destroys the singleton. + */ +void QDocDatabase::destroyQdocDB() +{ + if (s_qdocDB != nullptr) { + delete s_qdocDB; + s_qdocDB = nullptr; + } +} + +/*! + Initialize data structures in the singleton qdoc database. + + In particular, the type node map is initialized with a lot + type names that don't refer to documented types. For example, + many C++ standard types are included. These might be documented + here at some point, but for now they are not. Other examples + include \c array and \c data, which are just generic names + used as place holders in function signatures that appear in + the documentation. + + \note Do not add QML basic types into this list as it will + break linking to those types. + */ +void QDocDatabase::initializeDB() +{ + s_typeNodeMap.insert("accepted", nullptr); + s_typeNodeMap.insert("actionPerformed", nullptr); + s_typeNodeMap.insert("activated", nullptr); + s_typeNodeMap.insert("alias", nullptr); + s_typeNodeMap.insert("anchors", nullptr); + s_typeNodeMap.insert("any", nullptr); + s_typeNodeMap.insert("array", nullptr); + s_typeNodeMap.insert("autoSearch", nullptr); + s_typeNodeMap.insert("axis", nullptr); + s_typeNodeMap.insert("backClicked", nullptr); + s_typeNodeMap.insert("boomTime", nullptr); + s_typeNodeMap.insert("border", nullptr); + s_typeNodeMap.insert("buttonClicked", nullptr); + s_typeNodeMap.insert("callback", nullptr); + s_typeNodeMap.insert("char", nullptr); + s_typeNodeMap.insert("clicked", nullptr); + s_typeNodeMap.insert("close", nullptr); + s_typeNodeMap.insert("closed", nullptr); + s_typeNodeMap.insert("cond", nullptr); + s_typeNodeMap.insert("data", nullptr); + s_typeNodeMap.insert("dataReady", nullptr); + s_typeNodeMap.insert("dateString", nullptr); + s_typeNodeMap.insert("dateTimeString", nullptr); + s_typeNodeMap.insert("datetime", nullptr); + s_typeNodeMap.insert("day", nullptr); + s_typeNodeMap.insert("deactivated", nullptr); + s_typeNodeMap.insert("drag", nullptr); + s_typeNodeMap.insert("easing", nullptr); + s_typeNodeMap.insert("error", nullptr); + s_typeNodeMap.insert("exposure", nullptr); + s_typeNodeMap.insert("fatalError", nullptr); + s_typeNodeMap.insert("fileSelected", nullptr); + s_typeNodeMap.insert("flags", nullptr); + s_typeNodeMap.insert("float", nullptr); + s_typeNodeMap.insert("focus", nullptr); + s_typeNodeMap.insert("focusZone", nullptr); + s_typeNodeMap.insert("format", nullptr); + s_typeNodeMap.insert("framePainted", nullptr); + s_typeNodeMap.insert("from", nullptr); + s_typeNodeMap.insert("frontClicked", nullptr); + s_typeNodeMap.insert("function", nullptr); + s_typeNodeMap.insert("hasOpened", nullptr); + s_typeNodeMap.insert("hovered", nullptr); + s_typeNodeMap.insert("hoveredTitle", nullptr); + s_typeNodeMap.insert("hoveredUrl", nullptr); + s_typeNodeMap.insert("imageCapture", nullptr); + s_typeNodeMap.insert("imageProcessing", nullptr); + s_typeNodeMap.insert("index", nullptr); + s_typeNodeMap.insert("initialized", nullptr); + s_typeNodeMap.insert("isLoaded", nullptr); + s_typeNodeMap.insert("item", nullptr); + s_typeNodeMap.insert("key", nullptr); + s_typeNodeMap.insert("keysequence", nullptr); + s_typeNodeMap.insert("listViewClicked", nullptr); + s_typeNodeMap.insert("loadRequest", nullptr); + s_typeNodeMap.insert("locale", nullptr); + s_typeNodeMap.insert("location", nullptr); + s_typeNodeMap.insert("long", nullptr); + s_typeNodeMap.insert("message", nullptr); + s_typeNodeMap.insert("messageReceived", nullptr); + s_typeNodeMap.insert("mode", nullptr); + s_typeNodeMap.insert("month", nullptr); + s_typeNodeMap.insert("name", nullptr); + s_typeNodeMap.insert("number", nullptr); + s_typeNodeMap.insert("object", nullptr); + s_typeNodeMap.insert("offset", nullptr); + s_typeNodeMap.insert("ok", nullptr); + s_typeNodeMap.insert("openCamera", nullptr); + s_typeNodeMap.insert("openImage", nullptr); + s_typeNodeMap.insert("openVideo", nullptr); + s_typeNodeMap.insert("padding", nullptr); + s_typeNodeMap.insert("parent", nullptr); + s_typeNodeMap.insert("path", nullptr); + s_typeNodeMap.insert("photoModeSelected", nullptr); + s_typeNodeMap.insert("position", nullptr); + s_typeNodeMap.insert("precision", nullptr); + s_typeNodeMap.insert("presetClicked", nullptr); + s_typeNodeMap.insert("preview", nullptr); + s_typeNodeMap.insert("previewSelected", nullptr); + s_typeNodeMap.insert("progress", nullptr); + s_typeNodeMap.insert("puzzleLost", nullptr); + s_typeNodeMap.insert("qmlSignal", nullptr); + s_typeNodeMap.insert("rectangle", nullptr); + s_typeNodeMap.insert("request", nullptr); + s_typeNodeMap.insert("requestId", nullptr); + s_typeNodeMap.insert("section", nullptr); + s_typeNodeMap.insert("selected", nullptr); + s_typeNodeMap.insert("send", nullptr); + s_typeNodeMap.insert("settingsClicked", nullptr); + s_typeNodeMap.insert("shoe", nullptr); + s_typeNodeMap.insert("short", nullptr); + s_typeNodeMap.insert("signed", nullptr); + s_typeNodeMap.insert("sizeChanged", nullptr); + s_typeNodeMap.insert("size_t", nullptr); + s_typeNodeMap.insert("sockaddr", nullptr); + s_typeNodeMap.insert("someOtherSignal", nullptr); + s_typeNodeMap.insert("sourceSize", nullptr); + s_typeNodeMap.insert("startButtonClicked", nullptr); + s_typeNodeMap.insert("state", nullptr); + s_typeNodeMap.insert("std::initializer_list", nullptr); + s_typeNodeMap.insert("std::list", nullptr); + s_typeNodeMap.insert("std::map", nullptr); + s_typeNodeMap.insert("std::pair", nullptr); + s_typeNodeMap.insert("std::string", nullptr); + s_typeNodeMap.insert("std::vector", nullptr); + s_typeNodeMap.insert("stringlist", nullptr); + s_typeNodeMap.insert("swapPlayers", nullptr); + s_typeNodeMap.insert("symbol", nullptr); + s_typeNodeMap.insert("t", nullptr); + s_typeNodeMap.insert("T", nullptr); + s_typeNodeMap.insert("tagChanged", nullptr); + s_typeNodeMap.insert("timeString", nullptr); + s_typeNodeMap.insert("timeout", nullptr); + s_typeNodeMap.insert("to", nullptr); + s_typeNodeMap.insert("toggled", nullptr); + s_typeNodeMap.insert("type", nullptr); + s_typeNodeMap.insert("unsigned", nullptr); + s_typeNodeMap.insert("urllist", nullptr); + s_typeNodeMap.insert("va_list", nullptr); + s_typeNodeMap.insert("value", nullptr); + s_typeNodeMap.insert("valueEmitted", nullptr); + s_typeNodeMap.insert("videoFramePainted", nullptr); + s_typeNodeMap.insert("videoModeSelected", nullptr); + s_typeNodeMap.insert("videoRecorder", nullptr); + s_typeNodeMap.insert("void", nullptr); + s_typeNodeMap.insert("volatile", nullptr); + s_typeNodeMap.insert("wchar_t", nullptr); + s_typeNodeMap.insert("x", nullptr); + s_typeNodeMap.insert("y", nullptr); + s_typeNodeMap.insert("zoom", nullptr); + s_typeNodeMap.insert("zoomTo", nullptr); +} + +/*! \fn NamespaceNode *QDocDatabase::primaryTreeRoot() + Returns a pointer to the root node of the primary tree. + */ + +/*! + \fn const CNMap &QDocDatabase::groups() + Returns a const reference to the collection of all + group nodes in the primary tree. +*/ + +/*! + \fn const CNMap &QDocDatabase::modules() + Returns a const reference to the collection of all + module nodes in the primary tree. +*/ + +/*! + \fn const CNMap &QDocDatabase::qmlModules() + Returns a const reference to the collection of all + QML module nodes in the primary tree. +*/ + +/*! \fn CollectionNode *QDocDatabase::findGroup(const QString &name) + Find the group node named \a name and return a pointer + to it. If a matching node is not found, add a new group + node named \a name and return a pointer to that one. + + If a new group node is added, its parent is the tree root, + and the new group node is marked \e{not seen}. + */ + +/*! \fn CollectionNode *QDocDatabase::findModule(const QString &name) + Find the module node named \a name and return a pointer + to it. If a matching node is not found, add a new module + node named \a name and return a pointer to that one. + + If a new module node is added, its parent is the tree root, + and the new module node is marked \e{not seen}. + */ + +/*! \fn CollectionNode *QDocDatabase::addGroup(const QString &name) + Looks up the group named \a name in the primary tree. If + a match is found, a pointer to the node is returned. + Otherwise, a new group node named \a name is created and + inserted into the collection, and the pointer to that node + is returned. + */ + +/*! \fn CollectionNode *QDocDatabase::addModule(const QString &name) + Looks up the module named \a name in the primary tree. If + a match is found, a pointer to the node is returned. + Otherwise, a new module node named \a name is created and + inserted into the collection, and the pointer to that node + is returned. + */ + +/*! \fn CollectionNode *QDocDatabase::addQmlModule(const QString &name) + Looks up the QML module named \a name in the primary tree. + If a match is found, a pointer to the node is returned. + Otherwise, a new QML module node named \a name is created + and inserted into the collection, and the pointer to that + node is returned. + */ + +/*! \fn CollectionNode *QDocDatabase::addToGroup(const QString &name, Node *node) + Looks up the group node named \a name in the collection + of all group nodes. If a match is not found, a new group + node named \a name is created and inserted into the collection. + Then append \a node to the group's members list, and append the + group node to the member list of the \a node. The parent of the + \a node is not changed by this function. Returns a pointer to + the group node. + */ + +/*! \fn CollectionNode *QDocDatabase::addToModule(const QString &name, Node *node) + Looks up the module node named \a name in the collection + of all module nodes. If a match is not found, a new module + node named \a name is created and inserted into the collection. + Then append \a node to the module's members list. The parent of + \a node is not changed by this function. Returns the module node. + */ + +/*! \fn Collection *QDocDatabase::addToQmlModule(const QString &name, Node *node) + Looks up the QML module named \a name. If it isn't there, + create it. Then append \a node to the QML module's member + list. The parent of \a node is not changed by this function. + */ + +/*! \fn QmlTypeNode *QDocDatabase::findQmlType(const QString &name) + Returns the QML type node identified by the qualified + QML type \a name, or \c nullptr if no type was found. + */ + +/*! + Returns the QML type node identified by the QML module id + \a qmid and QML type \a name, or \c nullptr if no type + was found. + + If the QML module id is empty, looks up the QML type by + \a name only. + */ +QmlTypeNode *QDocDatabase::findQmlType(const QString &qmid, const QString &name) +{ + if (!qmid.isEmpty()) { + if (auto *qcn = m_forest.lookupQmlType(qmid + u"::"_s + name); qcn) + return qcn; + } + + QStringList path(name); + return static_cast<QmlTypeNode *>(m_forest.findNodeByNameAndType(path, &Node::isQmlType)); +} + +/*! + Returns the QML type node identified by the QML module id + constructed from the strings in the import \a record and the + QML type \a name. Returns \c nullptr if no type was not found. + */ +QmlTypeNode *QDocDatabase::findQmlType(const ImportRec &record, const QString &name) +{ + if (!record.isEmpty()) { + const QString qmName = record.m_importUri.isEmpty() ? + record.m_moduleName : record.m_importUri; + const QStringList dotSplit{name.split(QLatin1Char('.'))}; + for (const auto &namePart : dotSplit) { + if (auto *qcn = m_forest.lookupQmlType(qmName + u"::"_s + namePart); qcn) + return qcn; + } + } + return nullptr; +} + +/*! + Returns the QML node identified by the QML module id \a qmid + and \a name, searching in the primary tree only. If \a qmid + is an empty string, searches for the node using name only. + + Returns \c nullptr if no node was found. +*/ +QmlTypeNode *QDocDatabase::findQmlTypeInPrimaryTree(const QString &qmid, const QString &name) +{ + if (!qmid.isEmpty()) + return primaryTree()->lookupQmlType(qmid + u"::"_s + name); + return static_cast<QmlTypeNode *>(primaryTreeRoot()->findChildNode(name, Node::QML, TypesOnly)); +} + +/*! + This function calls a set of functions for each tree in the + forest that has not already been analyzed. In this way, when + running qdoc in \e singleExec mode, each tree is analyzed in + turn, and its classes and types are added to the appropriate + node maps. + */ +void QDocDatabase::processForest() +{ + processForest(&QDocDatabase::findAllClasses); + processForest(&QDocDatabase::findAllFunctions); + processForest(&QDocDatabase::findAllObsoleteThings); + processForest(&QDocDatabase::findAllLegaleseTexts); + processForest(&QDocDatabase::findAllSince); + processForest(&QDocDatabase::findAllAttributions); + resolveNamespaces(); +} + +/*! + This function calls \a func for each tree in the forest, + ensuring that \a func is called only once per tree. + + \sa processForest() + */ +void QDocDatabase::processForest(FindFunctionPtr func) +{ + Tree *t = m_forest.firstTree(); + while (t) { + if (!m_completedFindFunctions.values(t).contains(func)) { + (this->*(func))(t->root()); + m_completedFindFunctions.insert(t, func); + } + t = m_forest.nextTree(); + } +} + +/*! + Returns a reference to the collection of legalese texts. + */ +TextToNodeMap &QDocDatabase::getLegaleseTexts() +{ + processForest(&QDocDatabase::findAllLegaleseTexts); + return m_legaleseTexts; +} + +/*! + Returns a reference to the map of C++ classes with obsolete members. + */ +NodeMultiMap &QDocDatabase::getClassesWithObsoleteMembers() +{ + processForest(&QDocDatabase::findAllObsoleteThings); + return s_classesWithObsoleteMembers; +} + +/*! + Returns a reference to the map of obsolete QML types. + */ +NodeMultiMap &QDocDatabase::getObsoleteQmlTypes() +{ + processForest(&QDocDatabase::findAllObsoleteThings); + return s_obsoleteQmlTypes; +} + +/*! + Returns a reference to the map of QML types with obsolete members. + */ +NodeMultiMap &QDocDatabase::getQmlTypesWithObsoleteMembers() +{ + processForest(&QDocDatabase::findAllObsoleteThings); + return s_qmlTypesWithObsoleteMembers; +} + +/*! + Returns a reference to the map of QML basic types. + */ +NodeMultiMap &QDocDatabase::getQmlValueTypes() +{ + processForest(&QDocDatabase::findAllClasses); + return s_qmlBasicTypes; +} + +/*! + Returns a reference to the multimap of QML types. + */ +NodeMultiMap &QDocDatabase::getQmlTypes() +{ + processForest(&QDocDatabase::findAllClasses); + return s_qmlTypes; +} + +/*! + Returns a reference to the multimap of example nodes. + */ +NodeMultiMap &QDocDatabase::getExamples() +{ + processForest(&QDocDatabase::findAllClasses); + return s_examples; +} + +/*! + Returns a reference to the multimap of attribution nodes. + */ +NodeMultiMap &QDocDatabase::getAttributions() +{ + processForest(&QDocDatabase::findAllAttributions); + return m_attributions; +} + +/*! + Returns a reference to the map of obsolete C++ clases. + */ +NodeMultiMap &QDocDatabase::getObsoleteClasses() +{ + processForest(&QDocDatabase::findAllObsoleteThings); + return s_obsoleteClasses; +} + +/*! + Returns a reference to the map of all C++ classes. + */ +NodeMultiMap &QDocDatabase::getCppClasses() +{ + processForest(&QDocDatabase::findAllClasses); + return s_cppClasses; +} + +/*! + Returns the function index. This data structure is used to + output the function index page. + */ +NodeMapMap &QDocDatabase::getFunctionIndex() +{ + processForest(&QDocDatabase::findAllFunctions); + return m_functionIndex; +} + +/*! + Finds all the nodes containing legalese text and puts them + in a map. + */ +void QDocDatabase::findAllLegaleseTexts(Aggregate *node) +{ + for (const auto &childNode : node->childNodes()) { + if (childNode->isPrivate()) + continue; + if (!childNode->doc().legaleseText().isEmpty()) + m_legaleseTexts.insert(childNode->doc().legaleseText(), childNode); + if (childNode->isAggregate()) + findAllLegaleseTexts(static_cast<Aggregate *>(childNode)); + } +} + +/*! + \fn void QDocDatabase::findAllObsoleteThings(Aggregate *node) + + Finds all nodes with status = Deprecated and sorts them into + maps. They can be C++ classes, QML types, or they can be + functions, enum types, typedefs, methods, etc. + */ + +/*! + \fn void QDocDatabase::findAllSince(Aggregate *node) + + Finds all the nodes in \a node where a \e{since} command appeared + in the qdoc comment and sorts them into maps according to the kind + of node. + + This function is used for generating the "New Classes... in x.y" + section on the \e{What's New in Qt x.y} page. + */ + +/*! + Find the \a key in the map of new class maps, and return a + reference to the value, which is a NodeMap. If \a key is not + found, return a reference to an empty NodeMap. + */ +const NodeMultiMap &QDocDatabase::getClassMap(const QString &key) +{ + processForest(&QDocDatabase::findAllSince); + auto it = s_newClassMaps.constFind(key); + return (it != s_newClassMaps.constEnd()) ? it.value() : emptyNodeMultiMap_; +} + +/*! + Find the \a key in the map of new QML type maps, and return a + reference to the value, which is a NodeMap. If the \a key is not + found, return a reference to an empty NodeMap. + */ +const NodeMultiMap &QDocDatabase::getQmlTypeMap(const QString &key) +{ + processForest(&QDocDatabase::findAllSince); + auto it = s_newQmlTypeMaps.constFind(key); + return (it != s_newQmlTypeMaps.constEnd()) ? it.value() : emptyNodeMultiMap_; +} + +/*! + Find the \a key in the map of new \e {since} maps, and return + a reference to the value, which is a NodeMultiMap. If \a key + is not found, return a reference to an empty NodeMultiMap. + */ +const NodeMultiMap &QDocDatabase::getSinceMap(const QString &key) +{ + processForest(&QDocDatabase::findAllSince); + auto it = s_newSinceMaps.constFind(key); + return (it != s_newSinceMaps.constEnd()) ? it.value() : emptyNodeMultiMap_; +} + +/*! + Performs several housekeeping tasks prior to generating the + documentation. These tasks create required data structures + and resolve links. + */ +void QDocDatabase::resolveStuff() +{ + const auto &config = Config::instance(); + if (config.dualExec() || config.preparing()) { + // order matters + primaryTree()->resolveBaseClasses(primaryTreeRoot()); + primaryTree()->resolvePropertyOverriddenFromPtrs(primaryTreeRoot()); + primaryTreeRoot()->resolveRelates(); + primaryTreeRoot()->normalizeOverloads(); + primaryTree()->markDontDocumentNodes(); + primaryTree()->removePrivateAndInternalBases(primaryTreeRoot()); + primaryTree()->resolveProperties(); + primaryTreeRoot()->markUndocumentedChildrenInternal(); + primaryTreeRoot()->resolveQmlInheritance(); + primaryTree()->resolveTargets(primaryTreeRoot()); + primaryTree()->resolveCppToQmlLinks(); + primaryTree()->resolveSince(*primaryTreeRoot()); + } + if (config.singleExec() && config.generating()) { + primaryTree()->resolveBaseClasses(primaryTreeRoot()); + primaryTree()->resolvePropertyOverriddenFromPtrs(primaryTreeRoot()); + primaryTreeRoot()->resolveQmlInheritance(); + primaryTree()->resolveCppToQmlLinks(); + primaryTree()->resolveSince(*primaryTreeRoot()); + } + if (!config.preparing()) { + resolveNamespaces(); + resolveProxies(); + resolveBaseClasses(); + updateNavigation(); + } + if (config.dualExec()) + QDocIndexFiles::destroyQDocIndexFiles(); +} + +void QDocDatabase::resolveBaseClasses() +{ + Tree *t = m_forest.firstTree(); + while (t) { + t->resolveBaseClasses(t->root()); + t = m_forest.nextTree(); + } +} + +/*! + Returns a reference to the namespace map. Constructs the + namespace map if it hasn't been constructed yet. + + \note This function must not be called in the prepare phase. + */ +NodeMultiMap &QDocDatabase::getNamespaces() +{ + resolveNamespaces(); + return m_namespaceIndex; +} + +/*! + Multiple namespace nodes for namespace X can exist in the + qdoc database in different trees. This function first finds + all namespace nodes in all the trees and inserts them into + a multimap. Then it combines all the namespace nodes that + have the same name into a single namespace node of that + name and inserts that combined namespace node into an index. + */ +void QDocDatabase::resolveNamespaces() +{ + if (!m_namespaceIndex.isEmpty()) + return; + + bool linkErrors = !Config::instance().get(CONFIG_NOLINKERRORS).asBool(); + NodeMultiMap namespaceMultimap; + Tree *t = m_forest.firstTree(); + while (t) { + t->root()->findAllNamespaces(namespaceMultimap); + t = m_forest.nextTree(); + } + const QList<QString> keys = namespaceMultimap.uniqueKeys(); + for (const QString &key : keys) { + NamespaceNode *ns = nullptr; + NamespaceNode *indexNamespace = nullptr; + const NodeList namespaces = namespaceMultimap.values(key); + qsizetype count = namespaceMultimap.remove(key); + if (count > 0) { + for (auto *node : namespaces) { + ns = static_cast<NamespaceNode *>(node); + if (ns->isDocumentedHere()) + break; + else if (ns->hadDoc()) + indexNamespace = ns; // namespace was documented but in another tree + ns = nullptr; + } + if (ns) { + for (auto *node : namespaces) { + auto *nsNode = static_cast<NamespaceNode *>(node); + if (nsNode->hadDoc() && nsNode != ns) { + ns->doc().location().warning( + QStringLiteral("Namespace %1 documented more than once") + .arg(nsNode->name()), QStringLiteral("also seen here: %1") + .arg(nsNode->doc().location().toString())); + } + } + } else if (!indexNamespace) { + // Warn about documented children in undocumented namespaces. + // As the namespace can be documented outside this project, + // skip the warning if -no-link-errors is set + if (linkErrors) { + for (auto *node : namespaces) { + if (!node->isIndexNode()) + static_cast<NamespaceNode *>(node)->reportDocumentedChildrenInUndocumentedNamespace(); + } + } + } else { + for (auto *node : namespaces) { + auto *nsNode = static_cast<NamespaceNode *>(node); + if (nsNode != indexNamespace) + nsNode->setDocNode(indexNamespace); + } + } + } + /* + If there are multiple namespace nodes with the same + name where one of them will be the main reference page + for the namespace, include all nodes in the public + API of the namespace. + */ + if (ns && count > 1) { + for (auto *node : namespaces) { + auto *nameSpaceNode = static_cast<NamespaceNode *>(node); + if (nameSpaceNode != ns) { + for (auto it = nameSpaceNode->constBegin(); it != nameSpaceNode->constEnd(); + ++it) { + Node *anotherNs = *it; + if (anotherNs && anotherNs->isPublic() && !anotherNs->isInternal()) + ns->includeChild(anotherNs); + } + } + } + } + /* + Add the main namespace reference node to index, or the last seen + namespace if the main one was not found. + */ + if (!ns) + ns = indexNamespace ? indexNamespace : static_cast<NamespaceNode *>(namespaces.last()); + m_namespaceIndex.insert(ns->name(), ns); + } +} + +/*! + Each instance of class Tree that represents an index file + must be traversed to find all instances of class ProxyNode. + For each ProxyNode found, look up the ProxyNode's name in + the primary Tree. If it is found, it means that the proxy + node contains elements (normally just functions) that are + documented in the module represented by the Tree containing + the proxy node but that are related to the node we found in + the primary tree. + */ +void QDocDatabase::resolveProxies() +{ + // The first tree is the primary tree. + // Skip the primary tree. + Tree *t = m_forest.firstTree(); + t = m_forest.nextTree(); + while (t) { + const NodeList &proxies = t->proxies(); + if (!proxies.isEmpty()) { + for (auto *node : proxies) { + const auto *pn = static_cast<ProxyNode *>(node); + if (pn->count() > 0) { + Aggregate *aggregate = primaryTree()->findAggregate(pn->name()); + if (aggregate != nullptr) + aggregate->appendToRelatedByProxy(pn->childNodes()); + } + } + } + t = m_forest.nextTree(); + } +} + +/*! + Finds the function node for the qualified function path in + \a target and returns a pointer to it. The \a target is a + function signature with or without parameters but without + the return type. + + \a relative is the node in the primary tree where the search + begins. It is not used in the other trees, if the node is not + found in the primary tree. \a genus can be used to force the + search to find a C++ function or a QML function. + + The entire forest is searched, but the first match is accepted. + */ +const FunctionNode *QDocDatabase::findFunctionNode(const QString &target, const Node *relative, + Node::Genus genus) +{ + QString signature; + QString function = target; + qsizetype length = target.size(); + if (function.endsWith("()")) + function.chop(2); + if (function.endsWith(QChar(')'))) { + qsizetype position = function.lastIndexOf(QChar('(')); + signature = function.mid(position + 1, length - position - 2); + function = function.left(position); + } + QStringList path = function.split("::"); + return m_forest.findFunctionNode(path, Parameters(signature), relative, genus); +} + +/*! + This function is called for autolinking to a \a type, + which could be a function return type or a parameter + type. The tree node that represents the \a type is + returned. All the trees are searched until a match is + found. When searching the primary tree, the search + begins at \a relative and proceeds up the parent chain. + When searching the index trees, the search begins at the + root. + */ +const Node *QDocDatabase::findTypeNode(const QString &type, const Node *relative, Node::Genus genus) +{ + QStringList path = type.split("::"); + if ((path.size() == 1) && (path.at(0)[0].isLower() || path.at(0) == QString("T"))) { + auto it = s_typeNodeMap.find(path.at(0)); + if (it != s_typeNodeMap.end()) + return it.value(); + } + return m_forest.findTypeNode(path, relative, genus); +} + +/*! + Finds the node that will generate the documentation that + contains the \a target and returns a pointer to it. + + Can this be improved by using the target map in Tree? + */ +const Node *QDocDatabase::findNodeForTarget(const QString &target, const Node *relative) +{ + const Node *node = nullptr; + if (target.isEmpty()) + node = relative; + else if (target.endsWith(".html")) + node = findNodeByNameAndType(QStringList(target), &Node::isPageNode); + else { + QStringList path = target.split("::"); + int flags = SearchBaseClasses | SearchEnumValues; + for (const auto *tree : searchOrder()) { + const Node *n = tree->findNode(path, relative, flags, Node::DontCare); + if (n) + return n; + relative = nullptr; + } + node = findPageNodeByTitle(target); + } + return node; +} + +QStringList QDocDatabase::groupNamesForNode(Node *node) +{ + QStringList result; + CNMap *m = primaryTree()->getCollectionMap(Node::Group); + + if (!m) + return result; + + for (auto it = m->cbegin(); it != m->cend(); ++it) + if (it.value()->members().contains(node)) + result << it.key(); + + return result; +} + +/*! + Reads and parses the qdoc index files listed in \a indexFiles. + */ +void QDocDatabase::readIndexes(const QStringList &indexFiles) +{ + QStringList filesToRead; + for (const QString &file : indexFiles) { + QString fn = file.mid(file.lastIndexOf(QChar('/')) + 1); + if (!isLoaded(fn)) + filesToRead << file; + else + qCCritical(lcQdoc) << "Index file" << file << "is already in memory."; + } + QDocIndexFiles::qdocIndexFiles()->readIndexes(filesToRead); +} + +/*! + Generates a qdoc index file and write it to \a fileName. The + index file is generated with the parameters \a url and \a title, + using the generator \a g. + */ +void QDocDatabase::generateIndex(const QString &fileName, const QString &url, const QString &title, + Generator *g) +{ + QString t = fileName.mid(fileName.lastIndexOf(QChar('/')) + 1); + primaryTree()->setIndexFileName(t); + QDocIndexFiles::qdocIndexFiles()->generateIndex(fileName, url, title, g); + QDocIndexFiles::destroyQDocIndexFiles(); +} + +/*! + Returns the collection node representing the module that \a relative + node belongs to, or \c nullptr if there is no such module in the + primary tree. +*/ +const CollectionNode *QDocDatabase::getModuleNode(const Node *relative) +{ + Node::NodeType moduleType{Node::Module}; + QString moduleName; + switch (relative->genus()) + { + case Node::CPP: + moduleType = Node::Module; + moduleName = relative->physicalModuleName(); + break; + case Node::QML: + moduleType = Node::QmlModule; + moduleName = relative->logicalModuleName(); + break; + default: + return nullptr; + } + if (moduleName.isEmpty()) + return nullptr; + + return primaryTree()->getCollection(moduleName, moduleType); +} + +/*! + Finds all the collection nodes of the specified \a type + and merges them into the collection node map \a cnm. Nodes + that match the \a relative node are not included. + */ +void QDocDatabase::mergeCollections(Node::NodeType type, CNMap &cnm, const Node *relative) +{ + cnm.clear(); + CNMultiMap cnmm; + for (auto *tree : searchOrder()) { + CNMap *m = tree->getCollectionMap(type); + if (m && !m->isEmpty()) { + for (auto it = m->cbegin(); it != m->cend(); ++it) { + if (!it.value()->isInternal()) + cnmm.insert(it.key(), it.value()); + } + } + } + if (cnmm.isEmpty()) + return; + static const QRegularExpression singleDigit("\\b([0-9])\\b"); + const QStringList keys = cnmm.uniqueKeys(); + for (const auto &key : keys) { + const QList<CollectionNode *> values = cnmm.values(key); + CollectionNode *n = nullptr; + for (auto *value : values) { + if (value && value->wasSeen() && value != relative) { + n = value; + break; + } + } + if (n) { + if (values.size() > 1) { + for (CollectionNode *value : values) { + if (value != n) { + // Allow multiple (major) versions of QML modules + if ((n->isQmlModule()) + && n->logicalModuleIdentifier() != value->logicalModuleIdentifier()) { + if (value->wasSeen() && value != relative + && !value->members().isEmpty()) + cnm.insert(value->fullTitle().toLower(), value); + continue; + } + for (Node *t : value->members()) + n->addMember(t); + } + } + } + QString sortKey = n->fullTitle().toLower(); + if (sortKey.startsWith("the ")) + sortKey.remove(0, 4); + sortKey.replace(singleDigit, "0\\1"); + cnm.insert(sortKey, n); + } + } +} + +/*! + Finds all the collection nodes with the same name + and type as \a c and merges their members into the + members list of \a c. + + For QML modules, only nodes with matching + module identifiers are merged to avoid merging + modules with different (major) versions. + */ +void QDocDatabase::mergeCollections(CollectionNode *c) +{ + if (c == nullptr) + return; + + // REMARK: This form of merging is usually called during the + // generation phase om-the-fly when a source-of-truth collection + // is required. + // In practice, this means a collection could be merged many, many + // times during the lifetime of a generation. + // To avoid repeating the merging process each time, which could + // be time consuming, we use a small flag that is set directly on + // the collection to bail-out early. + // + // The merging process is only meaningful for collections when the + // collection references are spread troughout multiple projects. + // The part of information that exists in other project is read + // before the generation phase, such that when the generation + // phase comes, we already have all the information we need for + // merging such that we can consider all version of a certain + // collection node immutable, making the caching inherently + // correct at any point of the generation. + // + // This implies that this operation is unsafe if it is performed + // before all the index files are loaded. + // Indeed, this is a prerequisite, with the current structure, to + // perform this optmization. + // + // At the current time, this is true and is expected not to + // change. + // + // Do note that this is not applied to the other overload of + // mergeCollections as we cannot as safely ensure its consistency + // and, as the result of the merging depends on multiple + // parameters, it would require an actual memoization of the call. + // + // Note that this is a defensive optimization and we are assuming + // that it is effective based on heuristical data. As this is + // expected to disappear, at least in its current form, in the + // future, a more thorough analysis was not performed. + if (c->isMerged()) { + return; + } + + for (auto *tree : searchOrder()) { + CollectionNode *cn = tree->getCollection(c->name(), c->nodeType()); + if (cn && cn != c) { + if ((cn->isQmlModule()) + && cn->logicalModuleIdentifier() != c->logicalModuleIdentifier()) + continue; + + for (auto *node : cn->members()) + c->addMember(node); + + // REMARK: The merging process is performed to ensure that + // references to the collection in external projects are + // taken into account before consuming the collection. + // + // This works by having QDoc construct empty collections + // as soon as a reference to a collection is encountered + // and filling details later on when its definition is + // found. + // + // This initially-empty collection is always saved to the + // primaryTree and it is the collection that is directly + // accessible to consumers during the generation process. + // + // Nonetheless, when the definition for the collection is + // not in the same project as the one that is being + // compiled, its details will never be filled in. + // + // Indeed, the details will live in the index file for the + // project where the collection is defined, if any, and + // the node for it, which has complete information, will + // live in some non-primaryTree. + // + // The merging process itself is used by consumers during + // the generation process because they access the + // primaryTree version of the collection expecting a + // source-of-truth. + // To ensure that this is the case for usages that + // requires linking, we need to merge not only the members + // of the collection that reside in external versions of + // the collection; but some of the data that reside in the + // definition of the collection intself, namely the title + // and the url. + // + // A collection that contains the data of a definition is + // always marked as seen, hence we use that to discern + // whether we are working with a placeholder node or not, + // and fill in the data if we encounter a node that + // represents a definition. + // + // The way in which QDoc works implies that collection are + // globally scoped between projects. + // The repetition of the definition for the same + // collection is warned for as a duplicate documentation, + // such that we can expect a single valid source of truth + // for a given collection in each project. + // It is currently unknown if this warning is applicable + // when the repeated collection is defined in two + // different projects. + // + // As QDoc implicitly would not correctly support this + // case, we assume that only one declaration exists for + // each collection, such that the first encoutered one + // must be the source of truth and that there is no need + // to copy any data after the first copy is performed. + // KLUDGE: Note that this process is done as a hackish + // solution to QTBUG-104237 and should not be considered + // final or dependable. + if (!c->wasSeen() && cn->wasSeen()) { + c->markSeen(); + c->setTitle(cn->title()); + c->setUrl(cn->url()); + } + } + } + + c->markMerged(); +} + +/*! + Searches for the node that matches the path in \a atom and the + specified \a genus. The \a relative node is used if the first + leg of the path is empty, i.e. if the path begins with '#'. + The function also sets \a ref if there remains an unused leg + in the path after the node is found. The node is returned as + well as the \a ref. If the returned node pointer is null, + \a ref is also not valid. + */ +const Node *QDocDatabase::findNodeForAtom(const Atom *a, const Node *relative, QString &ref, + Node::Genus genus) +{ + const Node *node = nullptr; + + Atom *atom = const_cast<Atom *>(a); + QStringList targetPath = atom->string().split(QLatin1Char('#')); + QString first = targetPath.first().trimmed(); + + Tree *domain = nullptr; + + if (atom->isLinkAtom()) { + domain = atom->domain(); + genus = atom->genus(); + } + + if (first.isEmpty()) + node = relative; // search for a target on the current page. + else if (domain) { + if (first.endsWith(".html")) + node = domain->findNodeByNameAndType(QStringList(first), &Node::isPageNode); + else if (first.endsWith(QChar(')'))) { + QString signature; + QString function = first; + qsizetype length = first.size(); + if (function.endsWith("()")) + function.chop(2); + if (function.endsWith(QChar(')'))) { + qsizetype position = function.lastIndexOf(QChar('(')); + signature = function.mid(position + 1, length - position - 2); + function = function.left(position); + } + QStringList path = function.split("::"); + node = domain->findFunctionNode(path, Parameters(signature), nullptr, genus); + } + if (node == nullptr) { + int flags = SearchBaseClasses | SearchEnumValues; + QStringList nodePath = first.split("::"); + QString target; + targetPath.removeFirst(); + if (!targetPath.isEmpty()) + target = targetPath.takeFirst(); + if (relative && relative->tree()->physicalModuleName() != domain->physicalModuleName()) + relative = nullptr; + return domain->findNodeForTarget(nodePath, target, relative, flags, genus, ref); + } + } else { + if (first.endsWith(".html")) + node = findNodeByNameAndType(QStringList(first), &Node::isPageNode); + else if (first.endsWith(QChar(')'))) + node = findFunctionNode(first, relative, genus); + if (node == nullptr) + return findNodeForTarget(targetPath, relative, genus, ref); + } + + if (node != nullptr && ref.isEmpty()) { + if (!node->url().isEmpty()) + return node; + targetPath.removeFirst(); + if (!targetPath.isEmpty()) { + ref = node->root()->tree()->getRef(targetPath.first(), node); + if (ref.isEmpty()) + node = nullptr; + } + } + return node; +} + +/*! + Updates navigation (previous/next page links and the navigation parent) + for pages listed in the TOC, specified by the \c navigation.toctitles + configuration variable. + + if \c navigation.toctitles.inclusive is \c true, include also the TOC + page(s) themselves as a 'root' item in the navigation bar (breadcrumbs) + that are generated for HTML output. +*/ +void QDocDatabase::updateNavigation() +{ + // Restrict searching only to the local (primary) tree + QList<Tree *> searchOrder = this->searchOrder(); + setLocalSearch(); + + const QString configVar = CONFIG_NAVIGATION + + Config::dot + + CONFIG_TOCTITLES; + + // TODO: [direct-configuration-access] + // The configuration is currently a singleton with some generally + // global mutable state. + // + // Accessing the data in this form complicates testing and + // requires tests that inhibit any test parallelization, as the + // tests are not self contained. + // + // This should be generally avoived. Possibly, we should strive + // for Config to be a POD type that generally is scoped to + // main and whose data is destructured into dependencies when + // the dependencies are constructed. + bool inclusive{Config::instance().get( + configVar + Config::dot + CONFIG_INCLUSIVE).asBool()}; + + // TODO: [direct-configuration-access] + const auto tocTitles{Config::instance().get(configVar).asStringList()}; + + for (const auto &tocTitle : tocTitles) { + if (const auto candidateTarget = findNodeForTarget(tocTitle, nullptr); candidateTarget && candidateTarget->isPageNode()) { + auto tocPage{static_cast<const PageNode*>(candidateTarget)}; + + Text body = tocPage->doc().body(); + + auto *atom = body.firstAtom(); + + std::pair<PageNode *, Atom *> prev { nullptr, nullptr }; + + std::stack<const PageNode *> tocStack; + tocStack.push(inclusive ? tocPage : nullptr); + + bool inItem = false; + + // TODO: Understand how much we use this form of looping over atoms. + // If it is used a few times we might consider providing + // an iterator for Text to make use of a simpler + // range-for loop. + while (atom) { + switch (atom->type()) { + case Atom::ListItemLeft: + // Not known if we're going to have a link, push a temporary + tocStack.push(nullptr); + inItem = true; + break; + case Atom::ListItemRight: + tocStack.pop(); + inItem = false; + break; + case Atom::Link: { + if (!inItem) + break; + + // TODO: [unnecessary-output-parameter] + // We currently need an lvalue string to + // pass to findNodeForAtom, as the + // outparameter ref. + // + // Apart from the general problems with output + // parameters, we shouldn't be forced to + // instanciate an unnecessary object at call + // site. + // + // Understand what the correct way to avoid this is. + // This requires changes to findNodeForAtom + // and should be addressed in the context of + // revising that method. + QString unused{}; + // TODO: Having to const cast is really a code + // smell and could result in undefined + // behavior in some specific cases (e.g point + // to something that is actually const). + // + // We should understand how to sequence the + // code so that we have access to mutable data + // when we need it and "freeze" the data + // afterwards. + // + // If it we expect this form of mutability at + // this point we should expose a non-const API + // for the database, possibly limited to a + // very specific scope of execution. + // + // Understand what the correct sequencing for + // this processing is and revise this part. + auto candidatePage = const_cast<Node *>(findNodeForAtom(atom, nullptr, unused)); + if (!candidatePage || !candidatePage->isPageNode()) break; + + auto page{static_cast<PageNode*>(candidatePage)}; + + // ignore self-references + if (page == prev.first) break; + + if (prev.first) { + prev.first->setLink( + Node::NextLink, + page->title(), + // TODO: [possible-assertion-failure][imprecise-types][atoms-link] + // As with other structures in QDoc we + // are able to call methods that are + // valid only on very specific states. + // + // For some of those calls we have + // some defensive programming measures + // that allow us to at least identify + // the error during debugging, while + // for others this may currently hide + // some logic error. + // + // To avoid those cases, we should + // strive to move those cases to a + // compilation error, requiring a + // statically analyzable state that + // represents the current model. + // + // This would ensure that those + // lingering class of bugs are + // eliminated completely, forces a + // more explicit codebase where the + // current capabilities do not depend + // on runtime values might not be + // generally visible, and does not + // require us to incur into the + // required state, which may be rare, + // simplifying our abilities to + // evaluate all possible states. + // + // For linking atoms, LinkAtom is + // available and might be a good + // enough solution to move linkText + // to. + atom->linkText() + ); + page->setLink( + Node::PreviousLink, + prev.first->title(), + // TODO: [possible-assertion-failure][imprecise-types][atoms-link] + prev.second->linkText() + ); + } + + if (page == tocPage) + break; + + // Find the navigation parent from the stack; we may have null pointers + // for non-link list items, so skip those. + qsizetype popped = 0; + while (tocStack.size() > 1 && !tocStack.top()) { + tocStack.pop(); + ++popped; + } + + page->setNavigationParent(tocStack.empty() ? nullptr : tocStack.top()); + + while (--popped > 0) + tocStack.push(nullptr); + + tocStack.push(page); + prev = { page, atom }; + } + break; + default: + break; + } + + atom = atom->next(); + } + } else { + Config::instance().get(configVar).location() + .warning(QStringLiteral("Failed to find table of contents with title '%1'") + .arg(tocTitle)); + } + } + + // Restore search order + setSearchOrder(searchOrder); +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/qdocdatabase.h b/src/qdoc/qdoc/src/qdoc/qdocdatabase.h new file mode 100644 index 000000000..df2b4135c --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/qdocdatabase.h @@ -0,0 +1,395 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef QDOCDATABASE_H +#define QDOCDATABASE_H + +#include "config.h" +#include "examplenode.h" +#include "propertynode.h" +#include "text.h" +#include "tree.h" + +#include <QtCore/qdebug.h> +#include <QtCore/qmap.h> +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +typedef QMultiMap<Text, const Node *> TextToNodeMap; + +class Atom; +class FunctionNode; +class Generator; +class QDocDatabase; + +enum FindFlag { + SearchBaseClasses = 0x1, + SearchEnumValues = 0x2, + TypesOnly = 0x4, + IgnoreModules = 0x8 +}; + +class QDocForest +{ +private: + friend class QDocDatabase; + explicit QDocForest(QDocDatabase *qdb) : m_qdb(qdb), m_primaryTree(nullptr), m_currentIndex(0) + { + } + ~QDocForest(); + + Tree *firstTree(); + Tree *nextTree(); + Tree *primaryTree() { return m_primaryTree; } + Tree *findTree(const QString &t) { return m_forest.value(t); } + QStringList keys() { return m_forest.keys(); } + NamespaceNode *primaryTreeRoot() { return (m_primaryTree ? m_primaryTree->root() : nullptr); } + bool isEmpty() { return searchOrder().isEmpty(); } + bool done() { return (m_currentIndex >= searchOrder().size()); } + const QList<Tree *> &searchOrder(); + const QList<Tree *> &indexSearchOrder(); + void setSearchOrder(const QStringList &t); + bool isLoaded(const QString &fn) + { + return std::any_of(searchOrder().constBegin(), searchOrder().constEnd(), + [fn](Tree *tree) { return fn == tree->indexFileName(); }); + } + + const Node *findNode(const QStringList &path, const Node *relative, int findFlags, + Node::Genus genus) + { + for (const auto *tree : searchOrder()) { + const Node *n = tree->findNode(path, relative, findFlags, genus); + if (n) + return n; + relative = nullptr; + } + return nullptr; + } + + Node *findNodeByNameAndType(const QStringList &path, bool (Node::*isMatch)() const) + { + for (const auto *tree : searchOrder()) { + Node *n = tree->findNodeByNameAndType(path, isMatch); + if (n) + return n; + } + return nullptr; + } + + ClassNode *findClassNode(const QStringList &path) + { + for (const auto *tree : searchOrder()) { + ClassNode *n = tree->findClassNode(path); + if (n) + return n; + } + return nullptr; + } + + Node *findNodeForInclude(const QStringList &path) + { + for (const auto *tree : searchOrder()) { + Node *n = tree->findNodeForInclude(path); + if (n) + return n; + } + return nullptr; + } + + const FunctionNode *findFunctionNode(const QStringList &path, const Parameters ¶meters, + const Node *relative, Node::Genus genus); + const Node *findNodeForTarget(QStringList &targetPath, const Node *relative, Node::Genus genus, + QString &ref); + + const Node *findTypeNode(const QStringList &path, const Node *relative, Node::Genus genus) + { + int flags = SearchBaseClasses | SearchEnumValues | TypesOnly; + if (relative && genus == Node::DontCare && relative->genus() != Node::DOC) + genus = relative->genus(); + for (const auto *tree : searchOrder()) { + const Node *n = tree->findNode(path, relative, flags, genus); + if (n) + return n; + relative = nullptr; + } + return nullptr; + } + + const PageNode *findPageNodeByTitle(const QString &title) + { + for (const auto *tree : searchOrder()) { + const PageNode *n = tree->findPageNodeByTitle(title); + if (n) + return n; + } + return nullptr; + } + + const CollectionNode *getCollectionNode(const QString &name, Node::NodeType type) + { + for (auto *tree : searchOrder()) { + const CollectionNode *cn = tree->getCollection(name, type); + if (cn) + return cn; + } + return nullptr; + } + + QmlTypeNode *lookupQmlType(const QString &name) + { + for (const auto *tree : searchOrder()) { + QmlTypeNode *qcn = tree->lookupQmlType(name); + if (qcn) + return qcn; + } + return nullptr; + } + + void clearSearchOrder() { m_searchOrder.clear(); } + void newPrimaryTree(const QString &module); + void setPrimaryTree(const QString &t); + NamespaceNode *newIndexTree(const QString &module); + +private: + QDocDatabase *m_qdb; + Tree *m_primaryTree; + int m_currentIndex; + QMap<QString, Tree *> m_forest; + QList<Tree *> m_searchOrder; + QList<Tree *> m_indexSearchOrder; + QList<QString> m_moduleNames; +}; + +class QDocDatabase +{ +public: + static QDocDatabase *qdocDB(); + static void destroyQdocDB(); + ~QDocDatabase() = default; + + using FindFunctionPtr = void (QDocDatabase::*)(Aggregate *); + + Tree *findTree(const QString &t) { return m_forest.findTree(t); } + + const CNMap &groups() { return primaryTree()->groups(); } + const CNMap &modules() { return primaryTree()->modules(); } + const CNMap &qmlModules() { return primaryTree()->qmlModules(); } + + CollectionNode *addGroup(const QString &name) { return primaryTree()->addGroup(name); } + CollectionNode *addModule(const QString &name) { return primaryTree()->addModule(name); } + CollectionNode *addQmlModule(const QString &name) { return primaryTree()->addQmlModule(name); } + + CollectionNode *addToGroup(const QString &name, Node *node) + { + return primaryTree()->addToGroup(name, node); + } + CollectionNode *addToModule(const QString &name, Node *node) + { + return primaryTree()->addToModule(name, node); + } + CollectionNode *addToQmlModule(const QString &name, Node *node) + { + return primaryTree()->addToQmlModule(name, node); + } + + void addExampleNode(ExampleNode *n) { primaryTree()->addExampleNode(n); } + ExampleNodeMap &exampleNodeMap() { return primaryTree()->exampleNodeMap(); } + + QmlTypeNode *findQmlType(const QString &name) + { + return m_forest.lookupQmlType(name); + } + QmlTypeNode *findQmlType(const QString &qmid, const QString &name); + QmlTypeNode *findQmlType(const ImportRec &import, const QString &name); + QmlTypeNode *findQmlTypeInPrimaryTree(const QString &qmid, const QString &name); + + static NodeMultiMap &obsoleteClasses() { return s_obsoleteClasses; } + static NodeMultiMap &obsoleteQmlTypes() { return s_obsoleteQmlTypes; } + static NodeMultiMap &classesWithObsoleteMembers() { return s_classesWithObsoleteMembers; } + static NodeMultiMap &qmlTypesWithObsoleteMembers() { return s_qmlTypesWithObsoleteMembers; } + static NodeMultiMap &cppClasses() { return s_cppClasses; } + static NodeMultiMap &qmlBasicTypes() { return s_qmlBasicTypes; } + static NodeMultiMap &qmlTypes() { return s_qmlTypes; } + static NodeMultiMap &examples() { return s_examples; } + static NodeMultiMapMap &newClassMaps() { return s_newClassMaps; } + static NodeMultiMapMap &newQmlTypeMaps() { return s_newQmlTypeMaps; } + static NodeMultiMapMap &newEnumValueMaps() { return s_newEnumValueMaps; } + static NodeMultiMapMap &newSinceMaps() { return s_newSinceMaps; } + +private: + void findAllClasses(Aggregate *node) { node->findAllClasses(); } + void findAllFunctions(Aggregate *node) { node->findAllFunctions(m_functionIndex); } + void findAllAttributions(Aggregate *node) { node->findAllAttributions(m_attributions); } + void findAllLegaleseTexts(Aggregate *node); + void findAllObsoleteThings(Aggregate *node) { node->findAllObsoleteThings(); } + void findAllSince(Aggregate *node) { node->findAllSince(); } + +public: + /******************************************************************* + special collection access functions + ********************************************************************/ + NodeMultiMap &getCppClasses(); + NodeMultiMap &getObsoleteClasses(); + NodeMultiMap &getClassesWithObsoleteMembers(); + NodeMultiMap &getObsoleteQmlTypes(); + NodeMultiMap &getQmlTypesWithObsoleteMembers(); + NodeMultiMap &getNamespaces(); + NodeMultiMap &getQmlValueTypes(); + NodeMultiMap &getQmlTypes(); + NodeMultiMap &getExamples(); + NodeMultiMap &getAttributions(); + NodeMapMap &getFunctionIndex(); + TextToNodeMap &getLegaleseTexts(); + const NodeMultiMap &getClassMap(const QString &key); + const NodeMultiMap &getQmlTypeMap(const QString &key); + const NodeMultiMap &getSinceMap(const QString &key); + + /******************************************************************* + Many of these will be either eliminated or replaced. + ********************************************************************/ + void resolveStuff(); + void insertTarget(const QString &name, const QString &title, TargetRec::TargetType type, + Node *node, int priority) + { + primaryTree()->insertTarget(name, title, type, node, priority); + } + + /******************************************************************* + The functions declared below are called for the current tree only. + ********************************************************************/ + Aggregate *findRelatesNode(const QStringList &path) + { + return primaryTree()->findRelatesNode(path); + } + /*******************************************************************/ + + /***************************************************************************** + This function can handle parameters enclosed in '[' ']' (domain and genus). + ******************************************************************************/ + const Node *findNodeForAtom(const Atom *atom, const Node *relative, QString &ref, + Node::Genus genus = Node::DontCare); + /*******************************************************************/ + + /******************************************************************* + The functions declared below are called for all trees. + ********************************************************************/ + ClassNode *findClassNode(const QStringList &path) { return m_forest.findClassNode(path); } + Node *findNodeForInclude(const QStringList &path) { return m_forest.findNodeForInclude(path); } + const FunctionNode *findFunctionNode(const QString &target, const Node *relative, + Node::Genus genus); + const Node *findTypeNode(const QString &type, const Node *relative, Node::Genus genus); + const Node *findNodeForTarget(const QString &target, const Node *relative); + const PageNode *findPageNodeByTitle(const QString &title) + { + return m_forest.findPageNodeByTitle(title); + } + Node *findNodeByNameAndType(const QStringList &path, bool (Node::*isMatch)() const) + { + return m_forest.findNodeByNameAndType(path, isMatch); + } + const CollectionNode *getCollectionNode(const QString &name, Node::NodeType type) + { + return m_forest.getCollectionNode(name, type); + } + const CollectionNode *getModuleNode(const Node *relative); + + FunctionNode *findFunctionNodeForTag(const QString &tag) + { + return primaryTree()->findFunctionNodeForTag(tag); + } + FunctionNode *findMacroNode(const QString &t) { return primaryTree()->findMacroNode(t); } + + QStringList groupNamesForNode(Node *node); + +private: + const Node *findNodeForTarget(QStringList &targetPath, const Node *relative, Node::Genus genus, + QString &ref) + { + return m_forest.findNodeForTarget(targetPath, relative, genus, ref); + } + const FunctionNode *findFunctionNode(const QStringList &path, const Parameters ¶meters, + const Node *relative, Node::Genus genus) + { + return m_forest.findFunctionNode(path, parameters, relative, genus); + } + + /*******************************************************************/ +public: + void addPropertyFunction(PropertyNode *property, const QString &funcName, + PropertyNode::FunctionRole funcRole) + { + primaryTree()->addPropertyFunction(property, funcName, funcRole); + } + + void setVersion(const QString &v) { m_version = v; } + [[nodiscard]] QString version() const { return m_version; } + + void readIndexes(const QStringList &indexFiles); + void generateIndex(const QString &fileName, const QString &url, const QString &title, + Generator *g); + + void processForest(); + + NamespaceNode *primaryTreeRoot() { return m_forest.primaryTreeRoot(); } + void newPrimaryTree(const QString &module) { m_forest.newPrimaryTree(module); } + void setPrimaryTree(const QString &t) { m_forest.setPrimaryTree(t); } + NamespaceNode *newIndexTree(const QString &module) { return m_forest.newIndexTree(module); } + const QList<Tree *> &searchOrder() { return m_forest.searchOrder(); } + void setLocalSearch() { m_forest.m_searchOrder = QList<Tree *>(1, primaryTree()); } + void setSearchOrder(const QList<Tree *> &searchOrder) { m_forest.m_searchOrder = searchOrder; } + void setSearchOrder(QStringList &t) { m_forest.setSearchOrder(t); } + void mergeCollections(Node::NodeType type, CNMap &cnm, const Node *relative); + void mergeCollections(CollectionNode *c); + void clearSearchOrder() { m_forest.clearSearchOrder(); } + QStringList keys() { return m_forest.keys(); } + void resolveNamespaces(); + void resolveProxies(); + void resolveBaseClasses(); + void updateNavigation(); + +private: + friend class Tree; + + void processForest(FindFunctionPtr func); + bool isLoaded(const QString &t) { return m_forest.isLoaded(t); } + static void initializeDB(); + +private: + QDocDatabase(); + QDocDatabase(QDocDatabase const &) : m_forest(this) { } + QDocDatabase &operator=(QDocDatabase const &); + +public: + Tree *primaryTree() { return m_forest.primaryTree(); } + +private: + static QDocDatabase *s_qdocDB; + static NodeMap s_typeNodeMap; + static NodeMultiMap s_obsoleteClasses; + static NodeMultiMap s_classesWithObsoleteMembers; + static NodeMultiMap s_obsoleteQmlTypes; + static NodeMultiMap s_qmlTypesWithObsoleteMembers; + static NodeMultiMap s_cppClasses; + static NodeMultiMap s_qmlBasicTypes; + static NodeMultiMap s_qmlTypes; + static NodeMultiMap s_examples; + static NodeMultiMapMap s_newClassMaps; + static NodeMultiMapMap s_newQmlTypeMaps; + static NodeMultiMapMap s_newEnumValueMaps; + static NodeMultiMapMap s_newSinceMaps; + + QString m_version {}; + QDocForest m_forest; + + NodeMultiMap m_namespaceIndex {}; + NodeMultiMap m_attributions {}; + NodeMapMap m_functionIndex {}; + TextToNodeMap m_legaleseTexts {}; + QMultiHash<Tree*, FindFunctionPtr> m_completedFindFunctions {}; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/qdocindexfiles.cpp b/src/qdoc/qdoc/src/qdoc/qdocindexfiles.cpp new file mode 100644 index 000000000..f2f0d017e --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/qdocindexfiles.cpp @@ -0,0 +1,1458 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "qdocindexfiles.h" + +#include "access.h" +#include "atom.h" +#include "classnode.h" +#include "collectionnode.h" +#include "comparisoncategory.h" +#include "config.h" +#include "enumnode.h" +#include "examplenode.h" +#include "externalpagenode.h" +#include "functionnode.h" +#include "generator.h" +#include "headernode.h" +#include "location.h" +#include "utilities.h" +#include "propertynode.h" +#include "qdocdatabase.h" +#include "qmlpropertynode.h" +#include "typedefnode.h" +#include "variablenode.h" + +#include <QtCore/qxmlstream.h> + +#include <algorithm> + +QT_BEGIN_NAMESPACE + +enum QDocAttr { + QDocAttrNone, + QDocAttrExample, + QDocAttrFile, + QDocAttrImage, + QDocAttrDocument, + QDocAttrExternalPage, + QDocAttrAttribution +}; + +static Node *root_ = nullptr; +static IndexSectionWriter *post_ = nullptr; + +/*! + \class QDocIndexFiles + + This class handles qdoc index files. + */ + +QDocIndexFiles *QDocIndexFiles::s_qdocIndexFiles = nullptr; + +/*! + Constructs the singleton QDocIndexFiles. + */ +QDocIndexFiles::QDocIndexFiles() : m_gen(nullptr) +{ + m_qdb = QDocDatabase::qdocDB(); + m_storeLocationInfo = Config::instance().get(CONFIG_LOCATIONINFO).asBool(); +} + +/*! + Destroys the singleton QDocIndexFiles. + */ +QDocIndexFiles::~QDocIndexFiles() +{ + m_qdb = nullptr; + m_gen = nullptr; +} + +/*! + Creates the singleton. Allows only one instance of the class + to be created. Returns a pointer to the singleton. + */ +QDocIndexFiles *QDocIndexFiles::qdocIndexFiles() +{ + if (s_qdocIndexFiles == nullptr) + s_qdocIndexFiles = new QDocIndexFiles; + return s_qdocIndexFiles; +} + +/*! + Destroys the singleton. + */ +void QDocIndexFiles::destroyQDocIndexFiles() +{ + if (s_qdocIndexFiles != nullptr) { + delete s_qdocIndexFiles; + s_qdocIndexFiles = nullptr; + } +} + +/*! + Reads and parses the list of index files in \a indexFiles. + */ +void QDocIndexFiles::readIndexes(const QStringList &indexFiles) +{ + for (const QString &file : indexFiles) { + qCDebug(lcQdoc) << "Loading index file: " << file; + readIndexFile(file); + } +} + +/*! + Reads and parses the index file at \a path. + */ +void QDocIndexFiles::readIndexFile(const QString &path) +{ + QFile file(path); + if (!file.open(QFile::ReadOnly)) { + qWarning() << "Could not read index file" << path; + return; + } + + QXmlStreamReader reader(&file); + reader.setNamespaceProcessing(false); + + if (!reader.readNextStartElement()) + return; + + if (reader.name() != QLatin1String("INDEX")) + return; + + QXmlStreamAttributes attrs = reader.attributes(); + + QString indexUrl {attrs.value(QLatin1String("url")).toString()}; + + // Decide how we link to nodes loaded from this index file: + // If building a set that will be installed AND the URL of + // the dependency is identical to ours, assume that also + // the dependent html files are available under the same + // directory tree. Otherwise, link using the full index URL. + if (!Config::installDir.isEmpty() && indexUrl == Config::instance().get(CONFIG_URL).asString()) { + // Generate a relative URL between the install dir and the index file + // when the -installdir command line option is set. + QDir installDir(path.section('/', 0, -3) + '/' + Generator::outputSubdir()); + indexUrl = installDir.relativeFilePath(path).section('/', 0, -2); + } + m_project = attrs.value(QLatin1String("project")).toString(); + QString indexTitle = attrs.value(QLatin1String("indexTitle")).toString(); + m_basesList.clear(); + m_relatedNodes.clear(); + + NamespaceNode *root = m_qdb->newIndexTree(m_project); + if (!root) { + qWarning() << "Issue parsing index tree" << path; + return; + } + + root->tree()->setIndexTitle(indexTitle); + + // Scan all elements in the XML file, constructing a map that contains + // base classes for each class found. + while (reader.readNextStartElement()) { + readIndexSection(reader, root, indexUrl); + } + + // Now that all the base classes have been found for this index, + // arrange them into an inheritance hierarchy. + resolveIndex(); +} + +/*! + Read a <section> element from the index file and create the + appropriate node(s). + */ +void QDocIndexFiles::readIndexSection(QXmlStreamReader &reader, Node *current, + const QString &indexUrl) +{ + QXmlStreamAttributes attributes = reader.attributes(); + QStringView elementName = reader.name(); + + QString name = attributes.value(QLatin1String("name")).toString(); + QString href = attributes.value(QLatin1String("href")).toString(); + Node *node; + Location location; + Aggregate *parent = nullptr; + bool hasReadChildren = false; + + if (current->isAggregate()) + parent = static_cast<Aggregate *>(current); + + if (attributes.hasAttribute(QLatin1String("related"))) { + bool isIntTypeRelatedValue = false; + int relatedIndex = attributes.value(QLatin1String("related")).toInt(&isIntTypeRelatedValue); + if (isIntTypeRelatedValue) { + if (adoptRelatedNode(parent, relatedIndex)) { + reader.skipCurrentElement(); + return; + } + } else { + QList<Node *>::iterator nodeIterator = + std::find_if(m_relatedNodes.begin(), m_relatedNodes.end(), [&](const Node *relatedNode) { + return (name == relatedNode->name() && href == relatedNode->url().section(QLatin1Char('/'), -1)); + }); + + if (nodeIterator != m_relatedNodes.end() && parent) { + parent->adoptChild(*nodeIterator); + reader.skipCurrentElement(); + return; + } + } + } + + QString filePath; + int lineNo = 0; + if (attributes.hasAttribute(QLatin1String("filepath"))) { + filePath = attributes.value(QLatin1String("filepath")).toString(); + lineNo = attributes.value("lineno").toInt(); + } + if (elementName == QLatin1String("namespace")) { + auto *namespaceNode = new NamespaceNode(parent, name); + node = namespaceNode; + if (!indexUrl.isEmpty()) + location = Location(indexUrl + QLatin1Char('/') + name.toLower() + ".html"); + else if (!indexUrl.isNull()) + location = Location(name.toLower() + ".html"); + } else if (elementName == QLatin1String("class") || elementName == QLatin1String("struct") + || elementName == QLatin1String("union")) { + Node::NodeType type = Node::Class; + if (elementName == QLatin1String("class")) + type = Node::Class; + else if (elementName == QLatin1String("struct")) + type = Node::Struct; + else if (elementName == QLatin1String("union")) + type = Node::Union; + node = new ClassNode(type, parent, name); + if (attributes.hasAttribute(QLatin1String("bases"))) { + QString bases = attributes.value(QLatin1String("bases")).toString(); + if (!bases.isEmpty()) + m_basesList.append( + std::pair<ClassNode *, QString>(static_cast<ClassNode *>(node), bases)); + } + if (!indexUrl.isEmpty()) + location = Location(indexUrl + QLatin1Char('/') + name.toLower() + ".html"); + else if (!indexUrl.isNull()) + location = Location(name.toLower() + ".html"); + bool abstract = false; + if (attributes.value(QLatin1String("abstract")) == QLatin1String("true")) + abstract = true; + node->setAbstract(abstract); + } else if (elementName == QLatin1String("header")) { + node = new HeaderNode(parent, name); + + if (attributes.hasAttribute(QLatin1String("location"))) + name = attributes.value(QLatin1String("location")).toString(); + + if (!indexUrl.isEmpty()) + location = Location(indexUrl + QLatin1Char('/') + name); + else if (!indexUrl.isNull()) + location = Location(name); + } else if (elementName == QLatin1String("qmlclass") || elementName == QLatin1String("qmlvaluetype") + || elementName == QLatin1String("qmlbasictype")) { + auto *qmlTypeNode = new QmlTypeNode(parent, name, + elementName == QLatin1String("qmlclass") ? Node::QmlType : Node::QmlValueType); + qmlTypeNode->setTitle(attributes.value(QLatin1String("title")).toString()); + QString logicalModuleName = attributes.value(QLatin1String("qml-module-name")).toString(); + if (!logicalModuleName.isEmpty()) + m_qdb->addToQmlModule(logicalModuleName, qmlTypeNode); + bool abstract = false; + if (attributes.value(QLatin1String("abstract")) == QLatin1String("true")) + abstract = true; + qmlTypeNode->setAbstract(abstract); + QString qmlFullBaseName = attributes.value(QLatin1String("qml-base-type")).toString(); + if (!qmlFullBaseName.isEmpty()) { + qmlTypeNode->setQmlBaseName(qmlFullBaseName); + } + if (attributes.hasAttribute(QLatin1String("location"))) + name = attributes.value("location").toString(); + if (!indexUrl.isEmpty()) + location = Location(indexUrl + QLatin1Char('/') + name); + else if (!indexUrl.isNull()) + location = Location(name); + node = qmlTypeNode; + } else if (elementName == QLatin1String("qmlproperty")) { + QString type = attributes.value(QLatin1String("type")).toString(); + bool attached = false; + if (attributes.value(QLatin1String("attached")) == QLatin1String("true")) + attached = true; + bool readonly = false; + if (attributes.value(QLatin1String("writable")) == QLatin1String("false")) + readonly = true; + auto *qmlPropertyNode = new QmlPropertyNode(parent, name, type, attached); + qmlPropertyNode->markReadOnly(readonly); + if (attributes.value(QLatin1String("required")) == QLatin1String("true")) + qmlPropertyNode->setRequired(); + node = qmlPropertyNode; + } else if (elementName == QLatin1String("group")) { + auto *collectionNode = m_qdb->addGroup(name); + collectionNode->setTitle(attributes.value(QLatin1String("title")).toString()); + collectionNode->setSubtitle(attributes.value(QLatin1String("subtitle")).toString()); + if (attributes.value(QLatin1String("seen")) == QLatin1String("true")) + collectionNode->markSeen(); + node = collectionNode; + } else if (elementName == QLatin1String("module")) { + auto *collectionNode = m_qdb->addModule(name); + collectionNode->setTitle(attributes.value(QLatin1String("title")).toString()); + collectionNode->setSubtitle(attributes.value(QLatin1String("subtitle")).toString()); + if (attributes.value(QLatin1String("seen")) == QLatin1String("true")) + collectionNode->markSeen(); + node = collectionNode; + } else if (elementName == QLatin1String("qmlmodule")) { + auto *collectionNode = m_qdb->addQmlModule(name); + const QStringList info = QStringList() + << name + << QString(attributes.value(QLatin1String("qml-module-version")).toString()); + collectionNode->setLogicalModuleInfo(info); + collectionNode->setTitle(attributes.value(QLatin1String("title")).toString()); + collectionNode->setSubtitle(attributes.value(QLatin1String("subtitle")).toString()); + if (attributes.value(QLatin1String("seen")) == QLatin1String("true")) + collectionNode->markSeen(); + node = collectionNode; + } else if (elementName == QLatin1String("page")) { + QDocAttr subtype = QDocAttrNone; + QString attr = attributes.value(QLatin1String("subtype")).toString(); + if (attr == QLatin1String("attribution")) { + subtype = QDocAttrAttribution; + } else if (attr == QLatin1String("example")) { + subtype = QDocAttrExample; + } else if (attr == QLatin1String("file")) { + subtype = QDocAttrFile; + } else if (attr == QLatin1String("image")) { + subtype = QDocAttrImage; + } else if (attr == QLatin1String("page")) { + subtype = QDocAttrDocument; + } else if (attr == QLatin1String("externalpage")) { + subtype = QDocAttrExternalPage; + } else + goto done; + + if (current->isExample()) { + auto *exampleNode = static_cast<ExampleNode *>(current); + if (subtype == QDocAttrFile) { + exampleNode->appendFile(name); + goto done; + } else if (subtype == QDocAttrImage) { + exampleNode->appendImage(name); + goto done; + } + } + PageNode *pageNode = nullptr; + if (subtype == QDocAttrExample) + pageNode = new ExampleNode(parent, name); + else if (subtype == QDocAttrExternalPage) + pageNode = new ExternalPageNode(parent, name); + else { + pageNode = new PageNode(parent, name); + if (subtype == QDocAttrAttribution) pageNode->markAttribution(); + } + + pageNode->setTitle(attributes.value(QLatin1String("title")).toString()); + + if (attributes.hasAttribute(QLatin1String("location"))) + name = attributes.value(QLatin1String("location")).toString(); + + if (!indexUrl.isEmpty()) + location = Location(indexUrl + QLatin1Char('/') + name); + else if (!indexUrl.isNull()) + location = Location(name); + + node = pageNode; + + } else if (elementName == QLatin1String("enum")) { + auto *enumNode = new EnumNode(parent, name, attributes.hasAttribute("scoped")); + + if (!indexUrl.isEmpty()) + location = Location(indexUrl + QLatin1Char('/') + parent->name().toLower() + ".html"); + else if (!indexUrl.isNull()) + location = Location(parent->name().toLower() + ".html"); + + while (reader.readNextStartElement()) { + QXmlStreamAttributes childAttributes = reader.attributes(); + if (reader.name() == QLatin1String("value")) { + EnumItem item(childAttributes.value(QLatin1String("name")).toString(), + childAttributes.value(QLatin1String("value")).toString(), + childAttributes.value(QLatin1String("since")).toString() + ); + enumNode->addItem(item); + } else if (reader.name() == QLatin1String("keyword")) { + insertTarget(TargetRec::Keyword, childAttributes, enumNode); + } else if (reader.name() == QLatin1String("target")) { + insertTarget(TargetRec::Target, childAttributes, enumNode); + } + reader.skipCurrentElement(); + } + + node = enumNode; + + hasReadChildren = true; + } else if (elementName == QLatin1String("typedef")) { + if (attributes.hasAttribute("aliasedtype")) + node = new TypeAliasNode(parent, name, attributes.value(QLatin1String("aliasedtype")).toString()); + else + node = new TypedefNode(parent, name); + + if (!indexUrl.isEmpty()) + location = Location(indexUrl + QLatin1Char('/') + parent->name().toLower() + ".html"); + else if (!indexUrl.isNull()) + location = Location(parent->name().toLower() + ".html"); + } else if (elementName == QLatin1String("property")) { + auto *propNode = new PropertyNode(parent, name); + node = propNode; + if (attributes.value(QLatin1String("bindable")) == QLatin1String("true")) + propNode->setPropertyType(PropertyNode::PropertyType::BindableProperty); + + propNode->setWritable(attributes.value(QLatin1String("writable")) != QLatin1String("false")); + + if (!indexUrl.isEmpty()) + location = Location(indexUrl + QLatin1Char('/') + parent->name().toLower() + ".html"); + else if (!indexUrl.isNull()) + location = Location(parent->name().toLower() + ".html"); + + } else if (elementName == QLatin1String("function")) { + QString t = attributes.value(QLatin1String("meta")).toString(); + bool attached = false; + FunctionNode::Metaness metaness = FunctionNode::Plain; + if (!t.isEmpty()) + metaness = FunctionNode::getMetaness(t); + if (attributes.value(QLatin1String("attached")) == QLatin1String("true")) + attached = true; + auto *fn = new FunctionNode(metaness, parent, name, attached); + + fn->setReturnType(attributes.value(QLatin1String("type")).toString()); + + if (fn->isCppNode()) { + fn->setVirtualness(attributes.value(QLatin1String("virtual")).toString()); + fn->setConst(attributes.value(QLatin1String("const")) == QLatin1String("true")); + fn->setStatic(attributes.value(QLatin1String("static")) == QLatin1String("true")); + fn->setFinal(attributes.value(QLatin1String("final")) == QLatin1String("true")); + fn->setOverride(attributes.value(QLatin1String("override")) == QLatin1String("true")); + + if (attributes.value(QLatin1String("explicit")) == QLatin1String("true")) + fn->markExplicit(); + + if (attributes.value(QLatin1String("constexpr")) == QLatin1String("true")) + fn->markConstexpr(); + + if (attributes.value(QLatin1String("noexcept")) == QLatin1String("true")) { + fn->markNoexcept(attributes.value("noexcept_expression").toString()); + } + + qsizetype refness = attributes.value(QLatin1String("refness")).toUInt(); + if (refness == 1) + fn->setRef(true); + else if (refness == 2) + fn->setRefRef(true); + /* + Theoretically, this should ensure that each function + node receives the same overload number and overload + flag it was written with, and it should be unnecessary + to call normalizeOverloads() for index nodes. + */ + if (attributes.value(QLatin1String("overload")) == QLatin1String("true")) + fn->setOverloadNumber(attributes.value(QLatin1String("overload-number")).toUInt()); + else + fn->setOverloadNumber(0); + } + + /* + Note: The "signature" attribute was written to the + index file, but it is not read back in. That is ok + because we reconstruct the parameter list and the + return type, from which the signature was built in + the first place and from which it can be rebuilt. + */ + while (reader.readNextStartElement()) { + QXmlStreamAttributes childAttributes = reader.attributes(); + if (reader.name() == QLatin1String("parameter")) { + // Do not use the default value for the parameter; it is not + // required, and has been known to cause problems. + QString type = childAttributes.value(QLatin1String("type")).toString(); + QString name = childAttributes.value(QLatin1String("name")).toString(); + fn->parameters().append(type, name); + } else if (reader.name() == QLatin1String("keyword")) { + insertTarget(TargetRec::Keyword, childAttributes, fn); + } else if (reader.name() == QLatin1String("target")) { + insertTarget(TargetRec::Target, childAttributes, fn); + } + reader.skipCurrentElement(); + } + + node = fn; + if (!indexUrl.isEmpty()) + location = Location(indexUrl + QLatin1Char('/') + parent->name().toLower() + ".html"); + else if (!indexUrl.isNull()) + location = Location(parent->name().toLower() + ".html"); + + hasReadChildren = true; + } else if (elementName == QLatin1String("variable")) { + node = new VariableNode(parent, name); + if (!indexUrl.isEmpty()) + location = Location(indexUrl + QLatin1Char('/') + parent->name().toLower() + ".html"); + else if (!indexUrl.isNull()) + location = Location(parent->name().toLower() + ".html"); + } else if (elementName == QLatin1String("keyword")) { + insertTarget(TargetRec::Keyword, attributes, current); + goto done; + } else if (elementName == QLatin1String("target")) { + insertTarget(TargetRec::Target, attributes, current); + goto done; + } else if (elementName == QLatin1String("contents")) { + insertTarget(TargetRec::Contents, attributes, current); + goto done; + } else if (elementName == QLatin1String("proxy")) { + node = new ProxyNode(parent, name); + if (!indexUrl.isEmpty()) + location = Location(indexUrl + QLatin1Char('/') + name.toLower() + ".html"); + else if (!indexUrl.isNull()) + location = Location(name.toLower() + ".html"); + } else { + goto done; + } + + { + if (!href.isEmpty()) { + node->setUrl(href); + // Include the index URL if it exists + if (!node->isExternalPage() && !indexUrl.isEmpty()) + node->setUrl(indexUrl + QLatin1Char('/') + href); + } + + const QString access = attributes.value(QLatin1String("access")).toString(); + if (access == "protected") + node->setAccess(Access::Protected); + else if ((access == "private") || (access == "internal")) + node->setAccess(Access::Private); + else + node->setAccess(Access::Public); + + if (attributes.hasAttribute(QLatin1String("related"))) { + node->setRelatedNonmember(true); + m_relatedNodes << node; + } + + if (attributes.hasAttribute(QLatin1String("threadsafety"))) { + QString threadSafety = attributes.value(QLatin1String("threadsafety")).toString(); + if (threadSafety == QLatin1String("non-reentrant")) + node->setThreadSafeness(Node::NonReentrant); + else if (threadSafety == QLatin1String("reentrant")) + node->setThreadSafeness(Node::Reentrant); + else if (threadSafety == QLatin1String("thread safe")) + node->setThreadSafeness(Node::ThreadSafe); + else + node->setThreadSafeness(Node::UnspecifiedSafeness); + } else + node->setThreadSafeness(Node::UnspecifiedSafeness); + + const QString category = attributes.value(QLatin1String("comparison_category")).toString(); + node->setComparisonCategory(comparisonCategoryFromString(category.toStdString())); + + QString status = attributes.value(QLatin1String("status")).toString(); + // TODO: "obsolete" is kept for backward compatibility, remove in the near future + if (status == QLatin1String("obsolete") || status == QLatin1String("deprecated")) + node->setStatus(Node::Deprecated); + else if (status == QLatin1String("preliminary")) + node->setStatus(Node::Preliminary); + else if (status == QLatin1String("internal")) + node->setStatus(Node::Internal); + else if (status == QLatin1String("ignored")) + node->setStatus(Node::DontDocument); + else + node->setStatus(Node::Active); + + QString physicalModuleName = attributes.value(QLatin1String("module")).toString(); + if (!physicalModuleName.isEmpty()) + m_qdb->addToModule(physicalModuleName, node); + + QString since = attributes.value(QLatin1String("since")).toString(); + if (!since.isEmpty()) { + node->setSince(since); + } + + if (attributes.hasAttribute(QLatin1String("documented"))) { + if (attributes.value(QLatin1String("documented")) == QLatin1String("true")) + node->setHadDoc(); + } + + QString groupsAttr = attributes.value(QLatin1String("groups")).toString(); + if (!groupsAttr.isEmpty()) { + const QStringList groupNames = groupsAttr.split(QLatin1Char(',')); + for (const auto &group : groupNames) { + m_qdb->addToGroup(group, node); + } + } + + // Create some content for the node. + QSet<QString> emptySet; + Location t(filePath); + if (!filePath.isEmpty()) { + t.setLineNo(lineNo); + node->setLocation(t); + location = t; + } + Doc doc(location, location, QString(), emptySet, emptySet); // placeholder + node->setDoc(doc); + node->setIndexNodeFlag(); // Important: This node came from an index file. + QString briefAttr = attributes.value(QLatin1String("brief")).toString(); + if (!briefAttr.isEmpty()) { + node->setReconstitutedBrief(briefAttr); + } + + if (const auto sortKey = attributes.value(QLatin1String("sortkey")).toString(); !sortKey.isEmpty()) { + node->doc().constructExtra(); + if (auto *metaMap = node->doc().metaTagMap()) + metaMap->insert("sortkey", sortKey); + } + if (!hasReadChildren) { + bool useParent = (elementName == QLatin1String("namespace") && name.isEmpty()); + while (reader.readNextStartElement()) { + if (useParent) + readIndexSection(reader, parent, indexUrl); + else + readIndexSection(reader, node, indexUrl); + } + } + } + +done: + while (!reader.isEndElement()) { + if (reader.readNext() == QXmlStreamReader::Invalid) { + break; + } + } +} + +void QDocIndexFiles::insertTarget(TargetRec::TargetType type, + const QXmlStreamAttributes &attributes, Node *node) +{ + int priority; + switch (type) { + case TargetRec::Keyword: + priority = 1; + break; + case TargetRec::Target: + priority = 2; + break; + case TargetRec::Contents: + priority = 3; + break; + default: + return; + } + + QString name = attributes.value(QLatin1String("name")).toString(); + QString title = attributes.value(QLatin1String("title")).toString(); + m_qdb->insertTarget(name, title, type, node, priority); +} + +/*! + This function tries to resolve class inheritance immediately + after the index file is read. It is not always possible to + resolve a class inheritance at this point, because the base + class might be in an index file that hasn't been read yet, or + it might be in one of the header files that will be read for + the current module. These cases will be resolved after all + the index files and header and source files have been read, + just prior to beginning the generate phase for the current + module. + + I don't think this is completely correct because it always + sets the access to public. + */ +void QDocIndexFiles::resolveIndex() +{ + for (const auto &pair : std::as_const(m_basesList)) { + const QStringList bases = pair.second.split(QLatin1Char(',')); + for (const auto &base : bases) { + QStringList basePath = base.split(QString("::")); + Node *n = m_qdb->findClassNode(basePath); + if (n) + pair.first->addResolvedBaseClass(Access::Public, static_cast<ClassNode *>(n)); + else + pair.first->addUnresolvedBaseClass(Access::Public, basePath); + } + } + // No longer needed. + m_basesList.clear(); +} + +static QString getAccessString(Access t) +{ + + switch (t) { + case Access::Public: + return QLatin1String("public"); + case Access::Protected: + return QLatin1String("protected"); + case Access::Private: + return QLatin1String("private"); + default: + break; + } + return QLatin1String("public"); +} + +static QString getStatusString(Node::Status t) +{ + switch (t) { + case Node::Deprecated: + return QLatin1String("deprecated"); + case Node::Preliminary: + return QLatin1String("preliminary"); + case Node::Active: + return QLatin1String("active"); + case Node::Internal: + return QLatin1String("internal"); + case Node::DontDocument: + return QLatin1String("ignored"); + default: + break; + } + return QLatin1String("active"); +} + +static QString getThreadSafenessString(Node::ThreadSafeness t) +{ + switch (t) { + case Node::NonReentrant: + return QLatin1String("non-reentrant"); + case Node::Reentrant: + return QLatin1String("reentrant"); + case Node::ThreadSafe: + return QLatin1String("thread safe"); + case Node::UnspecifiedSafeness: + default: + break; + } + return QLatin1String("unspecified"); +} + +/*! + Returns the index of \a node in the list of related non-member nodes. +*/ +int QDocIndexFiles::indexForNode(Node *node) +{ + qsizetype i = m_relatedNodes.indexOf(node); + if (i == -1) { + i = m_relatedNodes.size(); + m_relatedNodes << node; + } + return i; +} + +/*! + Adopts the related non-member node identified by \a index to the + parent \a adoptiveParent. Returns \c true if successful. +*/ +bool QDocIndexFiles::adoptRelatedNode(Aggregate *adoptiveParent, int index) +{ + Node *related = m_relatedNodes.value(index); + + if (adoptiveParent && related) { + adoptiveParent->adoptChild(related); + return true; + } + + return false; +} + +/*! + Write canonicalized versions of \\target and \\keyword identifiers + that appear in the documentation of \a node into the index using + \a writer, so that they can be used as link targets in external + documentation sets. +*/ +void QDocIndexFiles::writeTargets(QXmlStreamWriter &writer, Node *node) +{ + if (node->doc().hasTargets()) { + for (const Atom *target : std::as_const(node->doc().targets())) { + const QString &title = target->string(); + const QString &name{Utilities::asAsciiPrintable(title)}; + writer.writeStartElement("target"); + writer.writeAttribute("name", node->isExternalPage() ? title : name); + if (name != title) + writer.writeAttribute("title", title); + writer.writeEndElement(); // target + } + } + if (node->doc().hasKeywords()) { + for (const Atom *keyword : std::as_const(node->doc().keywords())) { + const QString &title = keyword->string(); + const QString &name{Utilities::asAsciiPrintable(title)}; + writer.writeStartElement("keyword"); + writer.writeAttribute("name", name); + if (name != title) + writer.writeAttribute("title", title); + writer.writeEndElement(); // keyword + } + } +} + +/*! + Generate the index section with the given \a writer for the \a node + specified, returning true if an element was written, and returning + false if an element is not written. + + \note Function nodes are processed in generateFunctionSection() + */ +bool QDocIndexFiles::generateIndexSection(QXmlStreamWriter &writer, Node *node, + IndexSectionWriter *post) +{ + if (m_gen == nullptr) + m_gen = Generator::currentGenerator(); + + Q_ASSERT(m_gen); + + post_ = nullptr; + /* + Don't include index nodes in a new index file. + */ + if (node->isIndexNode()) + return false; + + QString nodeName; + QString logicalModuleName; + QString logicalModuleVersion; + QString qmlFullBaseName; + QString baseNameAttr; + QString moduleNameAttr; + QString moduleVerAttr; + + switch (node->nodeType()) { + case Node::Namespace: + nodeName = "namespace"; + break; + case Node::Class: + nodeName = "class"; + break; + case Node::Struct: + nodeName = "struct"; + break; + case Node::Union: + nodeName = "union"; + break; + case Node::HeaderFile: + nodeName = "header"; + break; + case Node::QmlType: + case Node::QmlValueType: + nodeName = (node->nodeType() == Node::QmlType) ? "qmlclass" : "qmlvaluetype"; + logicalModuleName = node->logicalModuleName(); + baseNameAttr = "qml-base-type"; + moduleNameAttr = "qml-module-name"; + moduleVerAttr = "qml-module-version"; + qmlFullBaseName = node->qmlFullBaseName(); + break; + case Node::Page: + case Node::Example: + case Node::ExternalPage: + nodeName = "page"; + break; + case Node::Group: + nodeName = "group"; + break; + case Node::Module: + nodeName = "module"; + break; + case Node::QmlModule: + nodeName = "qmlmodule"; + moduleNameAttr = "qml-module-name"; + moduleVerAttr = "qml-module-version"; + logicalModuleName = node->logicalModuleName(); + logicalModuleVersion = node->logicalModuleVersion(); + break; + case Node::Enum: + nodeName = "enum"; + break; + case Node::TypeAlias: + case Node::Typedef: + nodeName = "typedef"; + break; + case Node::Property: + nodeName = "property"; + break; + case Node::Variable: + nodeName = "variable"; + break; + case Node::SharedComment: + if (!node->isPropertyGroup()) + return false; + // Add an entry for property groups so that they can be linked to + nodeName = "qmlproperty"; + break; + case Node::QmlProperty: + nodeName = "qmlproperty"; + break; + case Node::Proxy: + nodeName = "proxy"; + break; + case Node::Function: // Now processed in generateFunctionSection() + default: + return false; + } + + QString objName = node->name(); + // Special case: only the root node should have an empty name. + if (objName.isEmpty() && node != m_qdb->primaryTreeRoot()) + return false; + + writer.writeStartElement(nodeName); + + if (!node->isTextPageNode() && !node->isCollectionNode() && !node->isHeader()) { + if (node->threadSafeness() != Node::UnspecifiedSafeness) + writer.writeAttribute("threadsafety", getThreadSafenessString(node->threadSafeness())); + } + + writer.writeAttribute("name", objName); + + // Write module and base type info for QML types + if (!moduleNameAttr.isEmpty()) { + if (!logicalModuleName.isEmpty()) + writer.writeAttribute(moduleNameAttr, logicalModuleName); + if (!logicalModuleVersion.isEmpty()) + writer.writeAttribute(moduleVerAttr, logicalModuleVersion); + } + if (!baseNameAttr.isEmpty() && !qmlFullBaseName.isEmpty()) + writer.writeAttribute(baseNameAttr, qmlFullBaseName); + + QString href; + if (!node->isExternalPage()) { + QString fullName = node->fullDocumentName(); + if (fullName != objName) + writer.writeAttribute("fullname", fullName); + href = m_gen->fullDocumentLocation(node); + } else + href = node->name(); + if (node->isQmlNode()) { + Aggregate *p = node->parent(); + if (p && p->isQmlType() && p->isAbstract()) + href.clear(); + } + if (!href.isEmpty()) + writer.writeAttribute("href", href); + + writer.writeAttribute("status", getStatusString(node->status())); + if (!node->isTextPageNode() && !node->isCollectionNode() && !node->isHeader()) { + writer.writeAttribute("access", getAccessString(node->access())); + if (node->isAbstract()) + writer.writeAttribute("abstract", "true"); + } + const Location &declLocation = node->declLocation(); + if (!declLocation.fileName().isEmpty()) + writer.writeAttribute("location", declLocation.fileName()); + if (m_storeLocationInfo && !declLocation.filePath().isEmpty()) { + writer.writeAttribute("filepath", declLocation.filePath()); + writer.writeAttribute("lineno", QString("%1").arg(declLocation.lineNo())); + } + + if (node->isRelatedNonmember()) + writer.writeAttribute("related", QString::number(indexForNode(node))); + + if (!node->since().isEmpty()) + writer.writeAttribute("since", node->since()); + + if (node->hasDoc()) + writer.writeAttribute("documented", "true"); + + QStringList groups = m_qdb->groupNamesForNode(node); + if (!groups.isEmpty()) + writer.writeAttribute("groups", groups.join(QLatin1Char(','))); + + if (const auto *metamap = node->doc().metaTagMap(); metamap) + if (const auto sortKey = metamap->value("sortkey"); !sortKey.isEmpty()) + writer.writeAttribute("sortkey", sortKey); + + QString brief = node->doc().trimmedBriefText(node->name()).toString(); + switch (node->nodeType()) { + case Node::Class: + case Node::Struct: + case Node::Union: { + // Classes contain information about their base classes. + const auto *classNode = static_cast<const ClassNode *>(node); + const QList<RelatedClass> &bases = classNode->baseClasses(); + QSet<QString> baseStrings; + for (const auto &related : bases) { + ClassNode *n = related.m_node; + if (n) + baseStrings.insert(n->fullName()); + else if (!related.m_path.isEmpty()) + baseStrings.insert(related.m_path.join(QLatin1String("::"))); + } + if (!baseStrings.isEmpty()) { + QStringList baseStringsAsList = baseStrings.values(); + baseStringsAsList.sort(); + writer.writeAttribute("bases", baseStringsAsList.join(QLatin1Char(','))); + } + if (!node->physicalModuleName().isEmpty()) + writer.writeAttribute("module", node->physicalModuleName()); + if (!brief.isEmpty()) + writer.writeAttribute("brief", brief); + if (auto category = node->comparisonCategory(); category != ComparisonCategory::None) + writer.writeAttribute("comparison_category", comparisonCategoryAsString(category)); + } break; + case Node::HeaderFile: { + const auto *headerNode = static_cast<const HeaderNode *>(node); + if (!headerNode->physicalModuleName().isEmpty()) + writer.writeAttribute("module", headerNode->physicalModuleName()); + if (!brief.isEmpty()) + writer.writeAttribute("brief", brief); + writer.writeAttribute("title", headerNode->title()); + writer.writeAttribute("fulltitle", headerNode->fullTitle()); + writer.writeAttribute("subtitle", headerNode->subtitle()); + } break; + case Node::Namespace: { + const auto *namespaceNode = static_cast<const NamespaceNode *>(node); + if (!namespaceNode->physicalModuleName().isEmpty()) + writer.writeAttribute("module", namespaceNode->physicalModuleName()); + if (!brief.isEmpty()) + writer.writeAttribute("brief", brief); + } break; + case Node::QmlValueType: + case Node::QmlType: { + const auto *qmlTypeNode = static_cast<const QmlTypeNode *>(node); + writer.writeAttribute("title", qmlTypeNode->title()); + writer.writeAttribute("fulltitle", qmlTypeNode->fullTitle()); + writer.writeAttribute("subtitle", qmlTypeNode->subtitle()); + if (!brief.isEmpty()) + writer.writeAttribute("brief", brief); + } break; + case Node::Page: + case Node::Example: + case Node::ExternalPage: { + if (node->isExample()) + writer.writeAttribute("subtype", "example"); + else if (node->isExternalPage()) + writer.writeAttribute("subtype", "externalpage"); + else + writer.writeAttribute("subtype", (static_cast<PageNode*>(node)->isAttribution() ? "attribution" : "page")); + + const auto *pageNode = static_cast<const PageNode *>(node); + writer.writeAttribute("title", pageNode->title()); + writer.writeAttribute("fulltitle", pageNode->fullTitle()); + writer.writeAttribute("subtitle", pageNode->subtitle()); + if (!brief.isEmpty()) + writer.writeAttribute("brief", brief); + } break; + case Node::Group: + case Node::Module: + case Node::QmlModule: { + const auto *collectionNode = static_cast<const CollectionNode *>(node); + writer.writeAttribute("seen", collectionNode->wasSeen() ? "true" : "false"); + writer.writeAttribute("title", collectionNode->title()); + if (!collectionNode->subtitle().isEmpty()) + writer.writeAttribute("subtitle", collectionNode->subtitle()); + if (!collectionNode->physicalModuleName().isEmpty()) + writer.writeAttribute("module", collectionNode->physicalModuleName()); + if (!brief.isEmpty()) + writer.writeAttribute("brief", brief); + } break; + case Node::QmlProperty: { + auto *qmlPropertyNode = static_cast<QmlPropertyNode *>(node); + writer.writeAttribute("type", qmlPropertyNode->dataType()); + writer.writeAttribute("attached", qmlPropertyNode->isAttached() ? "true" : "false"); + writer.writeAttribute("writable", qmlPropertyNode->isReadOnly() ? "false" : "true"); + if (qmlPropertyNode->isRequired()) + writer.writeAttribute("required", "true"); + if (!brief.isEmpty()) + writer.writeAttribute("brief", brief); + } break; + case Node::Property: { + const auto *propertyNode = static_cast<const PropertyNode *>(node); + + if (propertyNode->propertyType() == PropertyNode::PropertyType::BindableProperty) + writer.writeAttribute("bindable", "true"); + + if (!propertyNode->isWritable()) + writer.writeAttribute("writable", "false"); + + if (!brief.isEmpty()) + writer.writeAttribute("brief", brief); + // Property access function names + for (qsizetype i{0}; i < (qsizetype)PropertyNode::FunctionRole::NumFunctionRoles; ++i) { + auto role{(PropertyNode::FunctionRole)i}; + for (const auto *fnNode : propertyNode->functions(role)) { + writer.writeStartElement(PropertyNode::roleName(role)); + writer.writeAttribute("name", fnNode->name()); + writer.writeEndElement(); + } + } + } break; + case Node::Variable: { + const auto *variableNode = static_cast<const VariableNode *>(node); + writer.writeAttribute("type", variableNode->dataType()); + writer.writeAttribute("static", variableNode->isStatic() ? "true" : "false"); + if (!brief.isEmpty()) + writer.writeAttribute("brief", brief); + } break; + case Node::Enum: { + const auto *enumNode = static_cast<const EnumNode *>(node); + if (enumNode->isScoped()) + writer.writeAttribute("scoped", "true"); + if (enumNode->flagsType()) + writer.writeAttribute("typedef", enumNode->flagsType()->fullDocumentName()); + const auto &items = enumNode->items(); + for (const auto &item : items) { + writer.writeStartElement("value"); + writer.writeAttribute("name", item.name()); + writer.writeAttribute("value", item.value()); + if (!item.since().isEmpty()) + writer.writeAttribute("since", item.since()); + writer.writeEndElement(); // value + } + } break; + case Node::Typedef: { + const auto *typedefNode = static_cast<const TypedefNode *>(node); + if (typedefNode->associatedEnum()) + writer.writeAttribute("enum", typedefNode->associatedEnum()->fullDocumentName()); + } break; + case Node::TypeAlias: + writer.writeAttribute("aliasedtype", static_cast<const TypeAliasNode *>(node)->aliasedType()); + break; + case Node::Function: // Now processed in generateFunctionSection() + default: + break; + } + + writeTargets(writer, node); + + /* + Some nodes have a table of contents. For these, we close + the opening tag, create sub-elements for the items in the + table of contents, and then add a closing tag for the + element. Elements for all other nodes are closed in the + opening tag. + */ + if (node->isPageNode() || node->isCollectionNode()) { + if (node->doc().hasTableOfContents()) { + for (int i = 0; i < node->doc().tableOfContents().size(); ++i) { + Atom *item = node->doc().tableOfContents()[i]; + int level = node->doc().tableOfContentsLevels()[i]; + QString title = Text::sectionHeading(item).toString(); + writer.writeStartElement("contents"); + writer.writeAttribute("name", Tree::refForAtom(item)); + writer.writeAttribute("title", title); + writer.writeAttribute("level", QString::number(level)); + writer.writeEndElement(); // contents + } + } + } + // WebXMLGenerator - skip the nested <page> elements for example + // files/images, as the generator produces them separately + if (node->isExample() && m_gen->format() != QLatin1String("WebXML")) { + const auto *exampleNode = static_cast<const ExampleNode *>(node); + const auto &files = exampleNode->files(); + for (const QString &file : files) { + writer.writeStartElement("page"); + writer.writeAttribute("name", file); + QString href = m_gen->linkForExampleFile(file); + writer.writeAttribute("href", href); + writer.writeAttribute("status", "active"); + writer.writeAttribute("subtype", "file"); + writer.writeAttribute("title", ""); + writer.writeAttribute("fulltitle", Generator::exampleFileTitle(exampleNode, file)); + writer.writeAttribute("subtitle", file); + writer.writeEndElement(); // page + } + const auto &images = exampleNode->images(); + for (const QString &file : images) { + writer.writeStartElement("page"); + writer.writeAttribute("name", file); + QString href = m_gen->linkForExampleFile(file); + writer.writeAttribute("href", href); + writer.writeAttribute("status", "active"); + writer.writeAttribute("subtype", "image"); + writer.writeAttribute("title", ""); + writer.writeAttribute("fulltitle", Generator::exampleFileTitle(exampleNode, file)); + writer.writeAttribute("subtitle", file); + writer.writeEndElement(); // page + } + } + // Append to the section if the callback object was set + if (post) + post->append(writer, node); + + post_ = post; + return true; +} + +/*! + This function writes a <function> element for \a fn to the + index file using \a writer. + */ +void QDocIndexFiles::generateFunctionSection(QXmlStreamWriter &writer, FunctionNode *fn) +{ + if (fn->isInternal() && !Config::instance().showInternal()) + return; + + const QString objName = fn->name(); + writer.writeStartElement("function"); + writer.writeAttribute("name", objName); + + const QString fullName = fn->fullDocumentName(); + if (fullName != objName) + writer.writeAttribute("fullname", fullName); + const QString href = m_gen->fullDocumentLocation(fn); + if (!href.isEmpty()) + writer.writeAttribute("href", href); + if (fn->threadSafeness() != Node::UnspecifiedSafeness) + writer.writeAttribute("threadsafety", getThreadSafenessString(fn->threadSafeness())); + writer.writeAttribute("status", getStatusString(fn->status())); + writer.writeAttribute("access", getAccessString(fn->access())); + + const Location &declLocation = fn->declLocation(); + if (!declLocation.fileName().isEmpty()) + writer.writeAttribute("location", declLocation.fileName()); + if (m_storeLocationInfo && !declLocation.filePath().isEmpty()) { + writer.writeAttribute("filepath", declLocation.filePath()); + writer.writeAttribute("lineno", QString("%1").arg(declLocation.lineNo())); + } + + if (fn->hasDoc()) + writer.writeAttribute("documented", "true"); + if (fn->isRelatedNonmember()) + writer.writeAttribute("related", QString::number(indexForNode(fn))); + if (!fn->since().isEmpty()) + writer.writeAttribute("since", fn->since()); + + const QString brief = fn->doc().trimmedBriefText(fn->name()).toString(); + writer.writeAttribute("meta", fn->metanessString()); + if (fn->isCppNode()) { + if (!fn->isNonvirtual()) + writer.writeAttribute("virtual", fn->virtualness()); + + if (fn->isConst()) + writer.writeAttribute("const", "true"); + if (fn->isStatic()) + writer.writeAttribute("static", "true"); + if (fn->isFinal()) + writer.writeAttribute("final", "true"); + if (fn->isOverride()) + writer.writeAttribute("override", "true"); + if (fn->isExplicit()) + writer.writeAttribute("explicit", "true"); + if (fn->isConstexpr()) + writer.writeAttribute("constexpr", "true"); + + if (auto noexcept_info = fn->getNoexcept()) { + writer.writeAttribute("noexcept", "true"); + if (!(*noexcept_info).isEmpty()) writer.writeAttribute("noexcept_expression", *noexcept_info); + } + + /* + This ensures that for functions that have overloads, + the first function written is the one that is not an + overload, and the overloads follow it immediately in + the index file numbered from 1 to n. + */ + if (fn->isOverload() && (fn->overloadNumber() > 0)) { + writer.writeAttribute("overload", "true"); + writer.writeAttribute("overload-number", QString::number(fn->overloadNumber())); + } + if (fn->isRef()) + writer.writeAttribute("refness", QString::number(1)); + else if (fn->isRefRef()) + writer.writeAttribute("refness", QString::number(2)); + if (fn->hasAssociatedProperties()) { + QStringList associatedProperties; + for (const auto *node : fn->associatedProperties()) { + associatedProperties << node->name(); + } + associatedProperties.sort(); + writer.writeAttribute("associated-property", + associatedProperties.join(QLatin1Char(','))); + } + } + + const auto return_type = fn->returnType(); + if (!return_type.isEmpty()) + writer.writeAttribute("type", return_type); + + if (fn->isCppNode()) { + if (!brief.isEmpty()) + writer.writeAttribute("brief", brief); + + /* + Note: The "signature" attribute is written to the + index file, but it is not read back in by qdoc. However, + we need it for the webxml generator. + */ + const QString signature = appendAttributesToSignature(fn); + writer.writeAttribute("signature", signature); + + QStringList groups = m_qdb->groupNamesForNode(fn); + if (!groups.isEmpty()) + writer.writeAttribute("groups", groups.join(QLatin1Char(','))); + } + + for (int i = 0; i < fn->parameters().count(); ++i) { + const Parameter ¶meter = fn->parameters().at(i); + writer.writeStartElement("parameter"); + writer.writeAttribute("type", parameter.type()); + writer.writeAttribute("name", parameter.name()); + writer.writeAttribute("default", parameter.defaultValue()); + writer.writeEndElement(); // parameter + } + + writeTargets(writer, fn); + + // Append to the section if the callback object was set + if (post_) + post_->append(writer, fn); + + writer.writeEndElement(); // function +} + +/*! + \internal + + Constructs the signature to be written to an index file for the function + represented by FunctionNode \a fn. + + 'const' is already part of FunctionNode::signature(), which forms the basis + for the signature returned by this method. The method adds, where + applicable, the C++ keywords "final", "override", or "= 0", to the + signature carried by the FunctionNode itself. + */ +QString QDocIndexFiles::appendAttributesToSignature(const FunctionNode *fn) const noexcept +{ + QString signature = fn->signature(Node::SignatureReturnType); + + if (fn->isFinal()) + signature += " final"; + if (fn->isOverride()) + signature += " override"; + if (fn->isPureVirtual()) + signature += " = 0"; + + return signature; +} + +/*! + Outputs a <function> element to the index for each FunctionNode in + an \a aggregate, using \a writer. + The \a aggregate has a function map that contains all the + function nodes (a vector of overloads) indexed by function + name. + + If a function element represents an overload, it has an + \c overload attribute set to \c true and an \c {overload-number} + attribute set to the function's overload number. + */ +void QDocIndexFiles::generateFunctionSections(QXmlStreamWriter &writer, Aggregate *aggregate) +{ + for (auto functions : std::as_const(aggregate->functionMap())) { + std::for_each(functions.begin(), functions.end(), + [this,&writer](FunctionNode *fn) { + generateFunctionSection(writer, fn); + } + ); + } +} + +/*! + Generate index sections for the child nodes of the given \a node + using the \a writer specified. +*/ +void QDocIndexFiles::generateIndexSections(QXmlStreamWriter &writer, Node *node, + IndexSectionWriter *post) +{ + /* + Note that groups, modules, and QML modules are written + after all the other nodes. + */ + if (node->isCollectionNode() || node->isGroup() || node->isModule() || node->isQmlModule()) + return; + + if (node->isInternal() && !Config::instance().showInternal()) + return; + + if (generateIndexSection(writer, node, post)) { + if (node->isAggregate()) { + auto *aggregate = static_cast<Aggregate *>(node); + // First write the function children, then write the nonfunction children. + generateFunctionSections(writer, aggregate); + const auto &nonFunctionList = aggregate->nonfunctionList(); + for (auto *node : nonFunctionList) + generateIndexSections(writer, node, post); + } + + if (node == root_) { + /* + We wait until the end of the index file to output the group, module, + and QML module elements. By outputting them at the end, when we read + the index file back in, all the group, module, and QML module member + elements will have already been created. It is then only necessary to + create the group, module, or QML module element and add each member to + its member list. + */ + const CNMap &groups = m_qdb->groups(); + if (!groups.isEmpty()) { + for (auto it = groups.constBegin(); it != groups.constEnd(); ++it) { + if (generateIndexSection(writer, it.value(), post)) + writer.writeEndElement(); + } + } + + const CNMap &modules = m_qdb->modules(); + if (!modules.isEmpty()) { + for (auto it = modules.constBegin(); it != modules.constEnd(); ++it) { + if (generateIndexSection(writer, it.value(), post)) + writer.writeEndElement(); + } + } + + const CNMap &qmlModules = m_qdb->qmlModules(); + if (!qmlModules.isEmpty()) { + for (auto it = qmlModules.constBegin(); it != qmlModules.constEnd(); ++it) { + if (generateIndexSection(writer, it.value(), post)) + writer.writeEndElement(); + } + } + } + + writer.writeEndElement(); + } +} + +/*! + Writes a qdoc module index in XML to a file named \a fileName. + \a url is the \c url attribute of the <INDEX> element. + \a title is the \c title attribute of the <INDEX> element. + \a g is a pointer to the current Generator in use, stored for later use. + */ +void QDocIndexFiles::generateIndex(const QString &fileName, const QString &url, + const QString &title, Generator *g) +{ + QFile file(fileName); + if (!file.open(QFile::WriteOnly | QFile::Text)) + return; + + qCDebug(lcQdoc) << "Writing index file:" << fileName; + + m_gen = g; + m_relatedNodes.clear(); + QXmlStreamWriter writer(&file); + writer.setAutoFormatting(true); + writer.writeStartDocument(); + writer.writeDTD("<!DOCTYPE QDOCINDEX>"); + + writer.writeStartElement("INDEX"); + writer.writeAttribute("url", url); + writer.writeAttribute("title", title); + writer.writeAttribute("version", m_qdb->version()); + writer.writeAttribute("project", Config::instance().get(CONFIG_PROJECT).asString()); + + root_ = m_qdb->primaryTreeRoot(); + if (!root_->tree()->indexTitle().isEmpty()) + writer.writeAttribute("indexTitle", root_->tree()->indexTitle()); + + generateIndexSections(writer, root_, nullptr); + + writer.writeEndElement(); // INDEX + writer.writeEndElement(); // QDOCINDEX + writer.writeEndDocument(); + file.close(); +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/qdocindexfiles.h b/src/qdoc/qdoc/src/qdoc/qdocindexfiles.h new file mode 100644 index 000000000..2225aa576 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/qdocindexfiles.h @@ -0,0 +1,73 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef QDOCINDEXFILES_H +#define QDOCINDEXFILES_H + +#include "node.h" +#include "tree.h" + +QT_BEGIN_NAMESPACE + +class Atom; +class FunctionNode; +class Generator; +class QDocDatabase; +class WebXMLGenerator; +class QXmlStreamReader; +class QXmlStreamWriter; +class QXmlStreamAttributes; + +// A callback interface for extending index sections +class IndexSectionWriter +{ +public: + virtual ~IndexSectionWriter() = default; + virtual void append(QXmlStreamWriter &writer, Node *node) = 0; +}; + +class QDocIndexFiles +{ + friend class QDocDatabase; + friend class WebXMLGenerator; // for using generateIndexSections() + +private: + static QDocIndexFiles *qdocIndexFiles(); + static void destroyQDocIndexFiles(); + + QDocIndexFiles(); + ~QDocIndexFiles(); + + void readIndexes(const QStringList &indexFiles); + void readIndexFile(const QString &path); + void readIndexSection(QXmlStreamReader &reader, Node *current, const QString &indexUrl); + void insertTarget(TargetRec::TargetType type, const QXmlStreamAttributes &attributes, + Node *node); + void resolveIndex(); + int indexForNode(Node *node); + bool adoptRelatedNode(Aggregate *adoptiveParent, int index); + void writeTargets(QXmlStreamWriter &writer, Node *node); + + void generateIndex(const QString &fileName, const QString &url, const QString &title, + Generator *g); + void generateFunctionSection(QXmlStreamWriter &writer, FunctionNode *fn); + void generateFunctionSections(QXmlStreamWriter &writer, Aggregate *aggregate); + bool generateIndexSection(QXmlStreamWriter &writer, Node *node, + IndexSectionWriter *post = nullptr); + void generateIndexSections(QXmlStreamWriter &writer, Node *node, + IndexSectionWriter *post = nullptr); + QString appendAttributesToSignature(const FunctionNode *fn) const noexcept; + +private: + static QDocIndexFiles *s_qdocIndexFiles; + QDocDatabase *m_qdb {}; + Generator *m_gen {}; + QString m_project; + QList<std::pair<ClassNode *, QString>> m_basesList; + NodeList m_relatedNodes; + bool m_storeLocationInfo; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/qmlcodemarker.cpp b/src/qdoc/qdoc/src/qdoc/qmlcodemarker.cpp new file mode 100644 index 000000000..30dec979e --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/qmlcodemarker.cpp @@ -0,0 +1,175 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "qmlcodemarker.h" + +#include <QtCore/qregularexpression.h> + +#include "atom.h" +#include "node.h" +#include "qmlmarkupvisitor.h" +#include "text.h" + +#include <private/qqmljsast_p.h> +#include <private/qqmljsastfwd_p.h> +#include <private/qqmljsengine_p.h> +#include <private/qqmljslexer_p.h> +#include <private/qqmljsparser_p.h> + +QT_BEGIN_NAMESPACE + +/*! + Returns \c true if the \a code is recognized by the parser. + */ +bool QmlCodeMarker::recognizeCode(const QString &code) +{ + // Naive pre-check; starts with an import statement or 'CamelCase {' + static const QRegularExpression regExp(QStringLiteral("^\\s*(import |([A-Z][a-z0-9]*)+\\s?{)")); + if (!regExp.match(code).hasMatch()) + return false; + + QQmlJS::Engine engine; + QQmlJS::Lexer lexer(&engine); + QQmlJS::Parser parser(&engine); + + QString newCode = code; + extractPragmas(newCode); + lexer.setCode(newCode, 1); + + return parser.parse(); +} + +/*! + Returns \c true if \a ext is any of a list of file extensions + for the QML language. + */ +bool QmlCodeMarker::recognizeExtension(const QString &ext) +{ + return ext == "qml"; +} + +/*! + Returns \c true if the \a language is recognized. Only "QML" is + recognized by this marker. + */ +bool QmlCodeMarker::recognizeLanguage(const QString &language) +{ + return language == "QML"; +} + +/*! + Returns the type of atom used to represent QML code in the documentation. +*/ +Atom::AtomType QmlCodeMarker::atomType() const +{ + return Atom::Qml; +} + +QString QmlCodeMarker::markedUpCode(const QString &code, const Node *relative, + const Location &location) +{ + return addMarkUp(code, relative, location); +} + +/*! + Constructs and returns the marked up name for the \a node. + If the node is any kind of QML function (a method, + signal, or handler), "()" is appended to the marked up name. + */ +QString QmlCodeMarker::markedUpName(const Node *node) +{ + QString name = linkTag(node, taggedNode(node)); + if (node->isFunction()) + name += "()"; + return name; +} + +QString QmlCodeMarker::markedUpInclude(const QString &include) +{ + return addMarkUp("import " + include, nullptr, Location{}); +} + +QString QmlCodeMarker::addMarkUp(const QString &code, const Node * /* relative */, + const Location &location) +{ + QQmlJS::Engine engine; + QQmlJS::Lexer lexer(&engine); + + QString newCode = code; + QList<QQmlJS::SourceLocation> pragmas = extractPragmas(newCode); + lexer.setCode(newCode, 1); + + QQmlJS::Parser parser(&engine); + QString output; + + if (parser.parse()) { + QQmlJS::AST::UiProgram *ast = parser.ast(); + // Pass the unmodified code to the visitor so that pragmas and other + // unhandled source text can be output. + QmlMarkupVisitor visitor(code, pragmas, &engine); + QQmlJS::AST::Node::accept(ast, &visitor); + if (visitor.hasError()) { + location.warning( + location.fileName() + + QStringLiteral("Unable to analyze QML snippet. The output is incomplete.")); + } + output = visitor.markedUpCode(); + } else { + location.warning(QStringLiteral("Unable to parse QML snippet: \"%1\" at line %2, column %3") + .arg(parser.errorMessage()) + .arg(parser.errorLineNumber()) + .arg(parser.errorColumnNumber())); + output = protect(code); + } + + return output; +} + +/* + Copied and pasted from + src/declarative/qml/qqmlscriptparser.cpp. +*/ +void replaceWithSpace(QString &str, int idx, int n); // qmlcodeparser.cpp + +/* + Copied and pasted from + src/declarative/qml/qqmlscriptparser.cpp then modified to + return a list of removed pragmas. + + Searches for ".pragma <value>" or ".import <stuff>" declarations + in \a script. Currently supported pragmas are: library +*/ +QList<QQmlJS::SourceLocation> QmlCodeMarker::extractPragmas(QString &script) +{ + QList<QQmlJS::SourceLocation> removed; + + QQmlJS::Lexer l(nullptr); + l.setCode(script, 0); + + int token = l.lex(); + + while (true) { + if (token != QQmlJSGrammar::T_DOT) + break; + + int startOffset = l.tokenOffset(); + int startLine = l.tokenStartLine(); + int startColumn = l.tokenStartColumn(); + + token = l.lex(); + + if (token != QQmlJSGrammar::T_PRAGMA && token != QQmlJSGrammar::T_IMPORT) + break; + int endOffset = 0; + while (startLine == l.tokenStartLine()) { + endOffset = l.tokenLength() + l.tokenOffset(); + token = l.lex(); + } + replaceWithSpace(script, startOffset, endOffset - startOffset); + removed.append(QQmlJS::SourceLocation(startOffset, endOffset - startOffset, startLine, + startColumn)); + } + return removed; +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/qmlcodemarker.h b/src/qdoc/qdoc/src/qdoc/qmlcodemarker.h new file mode 100644 index 000000000..64a1f7c9f --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/qmlcodemarker.h @@ -0,0 +1,38 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef QMLCODEMARKER_H +#define QMLCODEMARKER_H + +#include "cppcodemarker.h" + +#include <private/qqmljsastfwd_p.h> + +QT_BEGIN_NAMESPACE + +class QmlCodeMarker : public CppCodeMarker +{ +public: + QmlCodeMarker() = default; + ~QmlCodeMarker() override = default; + + bool recognizeCode(const QString &code) override; + bool recognizeExtension(const QString &ext) override; + bool recognizeLanguage(const QString &language) override; + [[nodiscard]] Atom::AtomType atomType() const override; + QString markedUpCode(const QString &code, const Node *relative, + const Location &location) override; + + QString markedUpName(const Node *node) override; + QString markedUpInclude(const QString &include) override; + + /* Copied from src/declarative/qml/qdeclarativescriptparser.cpp */ + QList<QQmlJS::SourceLocation> extractPragmas(QString &script); + +private: + QString addMarkUp(const QString &code, const Node *relative, const Location &location); +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/qmlcodeparser.cpp b/src/qdoc/qdoc/src/qdoc/qmlcodeparser.cpp new file mode 100644 index 000000000..fadd7c307 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/qmlcodeparser.cpp @@ -0,0 +1,143 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "qmlcodeparser.h" + +#include "node.h" +#include "qmlvisitor.h" +#include "utilities.h" + +#include <private/qqmljsast_p.h> + +#include <qdebug.h> + +QT_BEGIN_NAMESPACE + +/*! + Returns "QML". + */ +QString QmlCodeParser::language() +{ + return "QML"; +} + +/*! + Returns a string list containing "*.qml". This is the only + file type parsed by the QMLN parser. + */ +QStringList QmlCodeParser::sourceFileNameFilter() +{ + return QStringList() << "*.qml"; +} + +/*! + Parses the source file at \a filePath and inserts the contents + into the database. The \a location is used for error reporting. + + If it can't open the file at \a filePath, it reports an error + and returns without doing anything. + */ +void QmlCodeParser::parseSourceFile(const Location &location, const QString &filePath, CppCodeParser&) +{ + static const QSet<QString> topic_commands{ + COMMAND_VARIABLE, COMMAND_QMLCLASS, COMMAND_QMLTYPE, COMMAND_QMLPROPERTY, + COMMAND_QMLPROPERTYGROUP, COMMAND_QMLATTACHEDPROPERTY, COMMAND_QMLSIGNAL, + COMMAND_QMLATTACHEDSIGNAL, COMMAND_QMLMETHOD, COMMAND_QMLATTACHEDMETHOD, + COMMAND_QMLVALUETYPE, COMMAND_QMLBASICTYPE, + }; + + QFile in(filePath); + if (!in.open(QIODevice::ReadOnly)) { + location.error(QStringLiteral("Cannot open QML file '%1'").arg(filePath)); + return; + } + + QString document = in.readAll(); + in.close(); + + QString newCode = document; + extractPragmas(newCode); + + QQmlJS::Engine engine{}; + QQmlJS::Lexer lexer{&engine}; + lexer.setCode(newCode, 1); + + QQmlJS::Parser parser{&engine}; + + if (parser.parse()) { + QQmlJS::AST::UiProgram *ast = parser.ast(); + QmlDocVisitor visitor(filePath, newCode, &engine, topic_commands + CodeParser::common_meta_commands, + topic_commands); + QQmlJS::AST::Node::accept(ast, &visitor); + if (visitor.hasError()) + Location(filePath).warning("Could not analyze QML file, output is incomplete."); + } + const auto &messages = parser.diagnosticMessages(); + for (const auto &msg : messages) { + qCDebug(lcQdoc, "%s: %d: %d: QML syntax error: %s", qUtf8Printable(filePath), + msg.loc.startLine, msg.loc.startColumn, qUtf8Printable(msg.message)); + } +} + +/*! + Copy and paste from src/declarative/qml/qdeclarativescriptparser.cpp. + This function blanks out the section of the \a str beginning at \a idx + and running for \a n characters. +*/ +void replaceWithSpace(QString &str, int idx, int n) // Also used in qmlcodemarker.cpp. +{ + QChar *data = str.data() + idx; + const QChar space(QLatin1Char(' ')); + for (int ii = 0; ii < n; ++ii) + *data++ = space; +} + +/*! + Copy & paste from src/declarative/qml/qdeclarativescriptparser.cpp, + then modified to return no values. + + Searches for ".pragma <value>" declarations within \a script. + Currently supported pragmas are: library +*/ +void QmlCodeParser::extractPragmas(QString &script) +{ + const QString pragma(QLatin1String("pragma")); + + QQmlJS::Lexer l(nullptr); + l.setCode(script, 0); + + int token = l.lex(); + + while (true) { + if (token != QQmlJSGrammar::T_DOT) + return; + + int startOffset = l.tokenOffset(); + int startLine = l.tokenStartLine(); + + token = l.lex(); + + if (token != QQmlJSGrammar::T_IDENTIFIER || l.tokenStartLine() != startLine + || script.mid(l.tokenOffset(), l.tokenLength()) != pragma) + return; + + token = l.lex(); + + if (token != QQmlJSGrammar::T_IDENTIFIER || l.tokenStartLine() != startLine) + return; + + QString pragmaValue = script.mid(l.tokenOffset(), l.tokenLength()); + int endOffset = l.tokenLength() + l.tokenOffset(); + + token = l.lex(); + if (l.tokenStartLine() == startLine) + return; + + if (pragmaValue == QLatin1String("library")) + replaceWithSpace(script, startOffset, endOffset - startOffset); + else + return; + } +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/qmlcodeparser.h b/src/qdoc/qdoc/src/qdoc/qmlcodeparser.h new file mode 100644 index 000000000..dff493be4 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/qmlcodeparser.h @@ -0,0 +1,38 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef QMLCODEPARSER_H +#define QMLCODEPARSER_H + +#include "codeparser.h" + +#include <QtCore/qset.h> + +#include <private/qqmljsengine_p.h> +#include <private/qqmljslexer_p.h> +#include <private/qqmljsparser_p.h> + +QT_BEGIN_NAMESPACE + +class Node; +class QString; + +class QmlCodeParser : public CodeParser +{ +public: + QmlCodeParser() = default; + ~QmlCodeParser() override = default; + + void initializeParser() override {} + void terminateParser() override {} + QString language() override; + QStringList sourceFileNameFilter() override; + void parseSourceFile(const Location &location, const QString &filePath, CppCodeParser&) override; + + /* Copied from src/declarative/qml/qdeclarativescriptparser.cpp */ + void extractPragmas(QString &script); +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/qmlmarkupvisitor.cpp b/src/qdoc/qdoc/src/qdoc/qmlmarkupvisitor.cpp new file mode 100644 index 000000000..31adb838d --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/qmlmarkupvisitor.cpp @@ -0,0 +1,794 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "qmlmarkupvisitor.h" + +#include <QtCore/qglobal.h> +#include <QtCore/qstringlist.h> + +#include <private/qqmljsast_p.h> +#include <private/qqmljsengine_p.h> + +QT_BEGIN_NAMESPACE + +QmlMarkupVisitor::QmlMarkupVisitor(const QString &source, + const QList<QQmlJS::SourceLocation> &pragmas, + QQmlJS::Engine *engine) +{ + this->m_source = source; + this->m_engine = engine; + + m_cursor = 0; + m_extraIndex = 0; + + // Merge the lists of locations of pragmas and comments in the source code. + int i = 0; + int j = 0; + const QList<QQmlJS::SourceLocation> comments = engine->comments(); + while (i < comments.size() && j < pragmas.size()) { + if (comments[i].offset < pragmas[j].offset) { + m_extraTypes.append(Comment); + m_extraLocations.append(comments[i]); + ++i; + } else { + m_extraTypes.append(Pragma); + m_extraLocations.append(comments[j]); + ++j; + } + } + + while (i < comments.size()) { + m_extraTypes.append(Comment); + m_extraLocations.append(comments[i]); + ++i; + } + + while (j < pragmas.size()) { + m_extraTypes.append(Pragma); + m_extraLocations.append(pragmas[j]); + ++j; + } +} + +// The protect() function is a copy of the one from CppCodeMarker. + +static const QString samp = QLatin1String("&"); +static const QString slt = QLatin1String("<"); +static const QString sgt = QLatin1String(">"); +static const QString squot = QLatin1String("""); + +QString QmlMarkupVisitor::protect(const QString &str) +{ + qsizetype n = str.size(); + QString marked; + marked.reserve(n * 2 + 30); + const QChar *data = str.constData(); + for (int i = 0; i != n; ++i) { + switch (data[i].unicode()) { + case '&': + marked += samp; + break; + case '<': + marked += slt; + break; + case '>': + marked += sgt; + break; + case '"': + marked += squot; + break; + default: + marked += data[i]; + } + } + return marked; +} + +QString QmlMarkupVisitor::markedUpCode() +{ + if (int(m_cursor) < m_source.size()) + addExtra(m_cursor, m_source.size()); + + return m_output; +} + +bool QmlMarkupVisitor::hasError() const +{ + return m_hasRecursionDepthError; +} + +void QmlMarkupVisitor::addExtra(quint32 start, quint32 finish) +{ + if (m_extraIndex >= m_extraLocations.size()) { + QString extra = m_source.mid(start, finish - start); + if (extra.trimmed().isEmpty()) + m_output += extra; + else + m_output += protect(extra); // text that should probably have been caught by the parser + + m_cursor = finish; + return; + } + + while (m_extraIndex < m_extraLocations.size()) { + if (m_extraTypes[m_extraIndex] == Comment) { + if (m_extraLocations[m_extraIndex].offset - 2 >= start) + break; + } else { + if (m_extraLocations[m_extraIndex].offset >= start) + break; + } + m_extraIndex++; + } + + quint32 i = start; + while (i < finish && m_extraIndex < m_extraLocations.size()) { + quint32 j = m_extraLocations[m_extraIndex].offset - 2; + if (i <= j && j < finish) { + if (i < j) + m_output += protect(m_source.mid(i, j - i)); + + quint32 l = m_extraLocations[m_extraIndex].length; + if (m_extraTypes[m_extraIndex] == Comment) { + if (m_source.mid(j, 2) == QLatin1String("/*")) + l += 4; + else + l += 2; + m_output += QLatin1String("<@comment>"); + m_output += protect(m_source.mid(j, l)); + m_output += QLatin1String("</@comment>"); + } else + m_output += protect(m_source.mid(j, l)); + + m_extraIndex++; + i = j + l; + } else + break; + } + + QString extra = m_source.mid(i, finish - i); + if (extra.trimmed().isEmpty()) + m_output += extra; + else + m_output += protect(extra); // text that should probably have been caught by the parser + + m_cursor = finish; +} + +void QmlMarkupVisitor::addMarkedUpToken(QQmlJS::SourceLocation &location, + const QString &tagName, + const QHash<QString, QString> &attributes) +{ + if (!location.isValid()) + return; + + if (m_cursor < location.offset) + addExtra(m_cursor, location.offset); + else if (m_cursor > location.offset) + return; + + m_output += QString(QLatin1String("<@%1")).arg(tagName); + for (const auto &key : attributes) + m_output += QString(QLatin1String(" %1=\"%2\"")).arg(key, attributes[key]); + m_output += QString(QLatin1String(">%2</@%3>")).arg(protect(sourceText(location)), tagName); + m_cursor += location.length; +} + +QString QmlMarkupVisitor::sourceText(QQmlJS::SourceLocation &location) +{ + return m_source.mid(location.offset, location.length); +} + +void QmlMarkupVisitor::addVerbatim(QQmlJS::SourceLocation first, + QQmlJS::SourceLocation last) +{ + if (!first.isValid()) + return; + + quint32 start = first.begin(); + quint32 finish; + if (last.isValid()) + finish = last.end(); + else + finish = first.end(); + + if (m_cursor < start) + addExtra(m_cursor, start); + else if (m_cursor > start) + return; + + QString text = m_source.mid(start, finish - start); + m_output += protect(text); + m_cursor = finish; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::UiImport *uiimport) +{ + addVerbatim(uiimport->importToken); + if (!uiimport->importUri) + addMarkedUpToken(uiimport->fileNameToken, QLatin1String("headerfile")); + return false; +} + +void QmlMarkupVisitor::endVisit(QQmlJS::AST::UiImport *uiimport) +{ + if (uiimport->version) + addVerbatim(uiimport->version->firstSourceLocation(), + uiimport->version->lastSourceLocation()); + addVerbatim(uiimport->asToken); + addMarkedUpToken(uiimport->importIdToken, QLatin1String("headerfile")); + addVerbatim(uiimport->semicolonToken); +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::UiPublicMember *member) +{ + if (member->type == QQmlJS::AST::UiPublicMember::Property) { + addVerbatim(member->defaultToken()); + addVerbatim(member->readonlyToken()); + addVerbatim(member->propertyToken()); + addVerbatim(member->typeModifierToken); + addMarkedUpToken(member->typeToken, QLatin1String("type")); + addMarkedUpToken(member->identifierToken, QLatin1String("name")); + addVerbatim(member->colonToken); + if (member->binding) + QQmlJS::AST::Node::accept(member->binding, this); + else if (member->statement) + QQmlJS::AST::Node::accept(member->statement, this); + } else { + addVerbatim(member->propertyToken()); + addVerbatim(member->typeModifierToken); + addMarkedUpToken(member->typeToken, QLatin1String("type")); + // addVerbatim(member->identifierToken()); + QQmlJS::AST::Node::accept(member->parameters, this); + } + addVerbatim(member->semicolonToken); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::UiObjectInitializer *initializer) +{ + addVerbatim(initializer->lbraceToken, initializer->lbraceToken); + return true; +} + +void QmlMarkupVisitor::endVisit(QQmlJS::AST::UiObjectInitializer *initializer) +{ + addVerbatim(initializer->rbraceToken, initializer->rbraceToken); +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::UiObjectBinding *binding) +{ + QQmlJS::AST::Node::accept(binding->qualifiedId, this); + addVerbatim(binding->colonToken); + QQmlJS::AST::Node::accept(binding->qualifiedTypeNameId, this); + QQmlJS::AST::Node::accept(binding->initializer, this); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::UiScriptBinding *binding) +{ + QQmlJS::AST::Node::accept(binding->qualifiedId, this); + addVerbatim(binding->colonToken); + QQmlJS::AST::Node::accept(binding->statement, this); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::UiArrayBinding *binding) +{ + QQmlJS::AST::Node::accept(binding->qualifiedId, this); + addVerbatim(binding->colonToken); + addVerbatim(binding->lbracketToken); + QQmlJS::AST::Node::accept(binding->members, this); + addVerbatim(binding->rbracketToken); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::UiArrayMemberList *list) +{ + for (QQmlJS::AST::UiArrayMemberList *it = list; it; it = it->next) { + QQmlJS::AST::Node::accept(it->member, this); + // addVerbatim(it->commaToken); + } + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::UiQualifiedId *id) +{ + addMarkedUpToken(id->identifierToken, QLatin1String("name")); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::ThisExpression *expression) +{ + addVerbatim(expression->thisToken); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::IdentifierExpression *identifier) +{ + addMarkedUpToken(identifier->identifierToken, QLatin1String("name")); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::NullExpression *null) +{ + addMarkedUpToken(null->nullToken, QLatin1String("number")); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::TrueLiteral *literal) +{ + addMarkedUpToken(literal->trueToken, QLatin1String("number")); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::FalseLiteral *literal) +{ + addMarkedUpToken(literal->falseToken, QLatin1String("number")); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::NumericLiteral *literal) +{ + addMarkedUpToken(literal->literalToken, QLatin1String("number")); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::StringLiteral *literal) +{ + addMarkedUpToken(literal->literalToken, QLatin1String("string")); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::RegExpLiteral *literal) +{ + addVerbatim(literal->literalToken); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::ArrayPattern *literal) +{ + addVerbatim(literal->lbracketToken); + QQmlJS::AST::Node::accept(literal->elements, this); + addVerbatim(literal->rbracketToken); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::ObjectPattern *literal) +{ + addVerbatim(literal->lbraceToken); + return true; +} + +void QmlMarkupVisitor::endVisit(QQmlJS::AST::ObjectPattern *literal) +{ + addVerbatim(literal->rbraceToken); +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::PatternElementList *list) +{ + for (QQmlJS::AST::PatternElementList *it = list; it; it = it->next) { + QQmlJS::AST::Node::accept(it->element, this); + // addVerbatim(it->commaToken); + } + QQmlJS::AST::Node::accept(list->elision, this); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::Elision *elision) +{ + addVerbatim(elision->commaToken, elision->commaToken); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::PatternProperty *list) +{ + QQmlJS::AST::Node::accept(list->name, this); + addVerbatim(list->colonToken, list->colonToken); + QQmlJS::AST::Node::accept(list->initializer, this); + // addVerbatim(list->commaToken, list->commaToken); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::ArrayMemberExpression *expression) +{ + QQmlJS::AST::Node::accept(expression->base, this); + addVerbatim(expression->lbracketToken); + QQmlJS::AST::Node::accept(expression->expression, this); + addVerbatim(expression->rbracketToken); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::FieldMemberExpression *expression) +{ + QQmlJS::AST::Node::accept(expression->base, this); + addVerbatim(expression->dotToken); + addMarkedUpToken(expression->identifierToken, QLatin1String("name")); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::NewMemberExpression *expression) +{ + addVerbatim(expression->newToken); + QQmlJS::AST::Node::accept(expression->base, this); + addVerbatim(expression->lparenToken); + QQmlJS::AST::Node::accept(expression->arguments, this); + addVerbatim(expression->rparenToken); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::NewExpression *expression) +{ + addVerbatim(expression->newToken); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::ArgumentList *list) +{ + addVerbatim(list->commaToken, list->commaToken); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::PostIncrementExpression *expression) +{ + addVerbatim(expression->incrementToken); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::PostDecrementExpression *expression) +{ + addVerbatim(expression->decrementToken); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::DeleteExpression *expression) +{ + addVerbatim(expression->deleteToken); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::VoidExpression *expression) +{ + addVerbatim(expression->voidToken); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::TypeOfExpression *expression) +{ + addVerbatim(expression->typeofToken); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::PreIncrementExpression *expression) +{ + addVerbatim(expression->incrementToken); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::PreDecrementExpression *expression) +{ + addVerbatim(expression->decrementToken); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::UnaryPlusExpression *expression) +{ + addVerbatim(expression->plusToken); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::UnaryMinusExpression *expression) +{ + addVerbatim(expression->minusToken); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::TildeExpression *expression) +{ + addVerbatim(expression->tildeToken); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::NotExpression *expression) +{ + addVerbatim(expression->notToken); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::BinaryExpression *expression) +{ + QQmlJS::AST::Node::accept(expression->left, this); + addMarkedUpToken(expression->operatorToken, QLatin1String("op")); + QQmlJS::AST::Node::accept(expression->right, this); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::ConditionalExpression *expression) +{ + QQmlJS::AST::Node::accept(expression->expression, this); + addVerbatim(expression->questionToken); + QQmlJS::AST::Node::accept(expression->ok, this); + addVerbatim(expression->colonToken); + QQmlJS::AST::Node::accept(expression->ko, this); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::Expression *expression) +{ + QQmlJS::AST::Node::accept(expression->left, this); + addVerbatim(expression->commaToken); + QQmlJS::AST::Node::accept(expression->right, this); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::Block *block) +{ + addVerbatim(block->lbraceToken); + return true; +} + +void QmlMarkupVisitor::endVisit(QQmlJS::AST::Block *block) +{ + addVerbatim(block->rbraceToken); +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::VariableStatement *statement) +{ + addVerbatim(statement->declarationKindToken); + QQmlJS::AST::Node::accept(statement->declarations, this); + // addVerbatim(statement->semicolonToken); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::VariableDeclarationList *list) +{ + for (QQmlJS::AST::VariableDeclarationList *it = list; it; it = it->next) { + QQmlJS::AST::Node::accept(it->declaration, this); + addVerbatim(it->commaToken); + } + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::EmptyStatement *statement) +{ + addVerbatim(statement->semicolonToken); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::ExpressionStatement *statement) +{ + QQmlJS::AST::Node::accept(statement->expression, this); + addVerbatim(statement->semicolonToken); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::IfStatement *statement) +{ + addMarkedUpToken(statement->ifToken, QLatin1String("keyword")); + addVerbatim(statement->lparenToken); + QQmlJS::AST::Node::accept(statement->expression, this); + addVerbatim(statement->rparenToken); + QQmlJS::AST::Node::accept(statement->ok, this); + if (statement->ko) { + addMarkedUpToken(statement->elseToken, QLatin1String("keyword")); + QQmlJS::AST::Node::accept(statement->ko, this); + } + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::DoWhileStatement *statement) +{ + addMarkedUpToken(statement->doToken, QLatin1String("keyword")); + QQmlJS::AST::Node::accept(statement->statement, this); + addMarkedUpToken(statement->whileToken, QLatin1String("keyword")); + addVerbatim(statement->lparenToken); + QQmlJS::AST::Node::accept(statement->expression, this); + addVerbatim(statement->rparenToken); + addVerbatim(statement->semicolonToken); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::WhileStatement *statement) +{ + addMarkedUpToken(statement->whileToken, QLatin1String("keyword")); + addVerbatim(statement->lparenToken); + QQmlJS::AST::Node::accept(statement->expression, this); + addVerbatim(statement->rparenToken); + QQmlJS::AST::Node::accept(statement->statement, this); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::ForStatement *statement) +{ + addMarkedUpToken(statement->forToken, QLatin1String("keyword")); + addVerbatim(statement->lparenToken); + QQmlJS::AST::Node::accept(statement->initialiser, this); + addVerbatim(statement->firstSemicolonToken); + QQmlJS::AST::Node::accept(statement->condition, this); + addVerbatim(statement->secondSemicolonToken); + QQmlJS::AST::Node::accept(statement->expression, this); + addVerbatim(statement->rparenToken); + QQmlJS::AST::Node::accept(statement->statement, this); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::ForEachStatement *statement) +{ + addMarkedUpToken(statement->forToken, QLatin1String("keyword")); + addVerbatim(statement->lparenToken); + QQmlJS::AST::Node::accept(statement->lhs, this); + addVerbatim(statement->inOfToken); + QQmlJS::AST::Node::accept(statement->expression, this); + addVerbatim(statement->rparenToken); + QQmlJS::AST::Node::accept(statement->statement, this); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::ContinueStatement *statement) +{ + addMarkedUpToken(statement->continueToken, QLatin1String("keyword")); + addMarkedUpToken(statement->identifierToken, QLatin1String("name")); + addVerbatim(statement->semicolonToken); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::BreakStatement *statement) +{ + addMarkedUpToken(statement->breakToken, QLatin1String("keyword")); + addMarkedUpToken(statement->identifierToken, QLatin1String("name")); + addVerbatim(statement->semicolonToken); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::ReturnStatement *statement) +{ + addMarkedUpToken(statement->returnToken, QLatin1String("keyword")); + QQmlJS::AST::Node::accept(statement->expression, this); + addVerbatim(statement->semicolonToken); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::WithStatement *statement) +{ + addMarkedUpToken(statement->withToken, QLatin1String("keyword")); + addVerbatim(statement->lparenToken); + QQmlJS::AST::Node::accept(statement->expression, this); + addVerbatim(statement->rparenToken); + QQmlJS::AST::Node::accept(statement->statement, this); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::CaseBlock *block) +{ + addVerbatim(block->lbraceToken); + return true; +} + +void QmlMarkupVisitor::endVisit(QQmlJS::AST::CaseBlock *block) +{ + addVerbatim(block->rbraceToken, block->rbraceToken); +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::SwitchStatement *statement) +{ + addMarkedUpToken(statement->switchToken, QLatin1String("keyword")); + addVerbatim(statement->lparenToken); + QQmlJS::AST::Node::accept(statement->expression, this); + addVerbatim(statement->rparenToken); + QQmlJS::AST::Node::accept(statement->block, this); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::CaseClause *clause) +{ + addMarkedUpToken(clause->caseToken, QLatin1String("keyword")); + QQmlJS::AST::Node::accept(clause->expression, this); + addVerbatim(clause->colonToken); + QQmlJS::AST::Node::accept(clause->statements, this); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::DefaultClause *clause) +{ + addMarkedUpToken(clause->defaultToken, QLatin1String("keyword")); + addVerbatim(clause->colonToken, clause->colonToken); + return true; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::LabelledStatement *statement) +{ + addMarkedUpToken(statement->identifierToken, QLatin1String("name")); + addVerbatim(statement->colonToken); + QQmlJS::AST::Node::accept(statement->statement, this); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::ThrowStatement *statement) +{ + addMarkedUpToken(statement->throwToken, QLatin1String("keyword")); + QQmlJS::AST::Node::accept(statement->expression, this); + addVerbatim(statement->semicolonToken); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::Catch *c) +{ + addMarkedUpToken(c->catchToken, QLatin1String("keyword")); + addVerbatim(c->lparenToken); + addMarkedUpToken(c->identifierToken, QLatin1String("name")); + addVerbatim(c->rparenToken); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::Finally *f) +{ + addMarkedUpToken(f->finallyToken, QLatin1String("keyword")); + QQmlJS::AST::Node::accept(f->statement, this); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::TryStatement *statement) +{ + addMarkedUpToken(statement->tryToken, QLatin1String("keyword")); + QQmlJS::AST::Node::accept(statement->statement, this); + QQmlJS::AST::Node::accept(statement->catchExpression, this); + QQmlJS::AST::Node::accept(statement->finallyExpression, this); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::FunctionExpression *expression) +{ + addMarkedUpToken(expression->functionToken, QLatin1String("keyword")); + addMarkedUpToken(expression->identifierToken, QLatin1String("name")); + addVerbatim(expression->lparenToken); + QQmlJS::AST::Node::accept(expression->formals, this); + addVerbatim(expression->rparenToken); + addVerbatim(expression->lbraceToken); + QQmlJS::AST::Node::accept(expression->body, this); + addVerbatim(expression->rbraceToken); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::FunctionDeclaration *declaration) +{ + addMarkedUpToken(declaration->functionToken, QLatin1String("keyword")); + addMarkedUpToken(declaration->identifierToken, QLatin1String("name")); + addVerbatim(declaration->lparenToken); + QQmlJS::AST::Node::accept(declaration->formals, this); + addVerbatim(declaration->rparenToken); + addVerbatim(declaration->lbraceToken); + QQmlJS::AST::Node::accept(declaration->body, this); + addVerbatim(declaration->rbraceToken); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::FormalParameterList *list) +{ + // addVerbatim(list->commaToken); + QQmlJS::AST::Node::accept(list->element, this); + // addMarkedUpToken(list->identifierToken, QLatin1String("name")); + return false; +} + +bool QmlMarkupVisitor::visit(QQmlJS::AST::DebuggerStatement *statement) +{ + addVerbatim(statement->debuggerToken); + addVerbatim(statement->semicolonToken); + return true; +} + +// Elements and items are represented by UiObjectDefinition nodes. + +bool QmlMarkupVisitor::visit(QQmlJS::AST::UiObjectDefinition *definition) +{ + addMarkedUpToken(definition->qualifiedTypeNameId->identifierToken, QLatin1String("type")); + QQmlJS::AST::Node::accept(definition->initializer, this); + return false; +} + +void QmlMarkupVisitor::throwRecursionDepthError() +{ + m_hasRecursionDepthError = true; +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/qmlmarkupvisitor.h b/src/qdoc/qdoc/src/qdoc/qmlmarkupvisitor.h new file mode 100644 index 000000000..a19636a67 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/qmlmarkupvisitor.h @@ -0,0 +1,139 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef QMLMARKUPVISITOR_H +#define QMLMARKUPVISITOR_H + +#include "node.h" +#include "tree.h" + +#include <QtCore/qstring.h> + +#include <private/qqmljsastvisitor_p.h> +#include <private/qqmljsengine_p.h> + +QT_BEGIN_NAMESPACE + +class QmlMarkupVisitor : public QQmlJS::AST::Visitor +{ +public: + enum ExtraType { Comment, Pragma }; + + QmlMarkupVisitor(const QString &code, const QList<QQmlJS::SourceLocation> &pragmas, + QQmlJS::Engine *engine); + ~QmlMarkupVisitor() override = default; + + QString markedUpCode(); + [[nodiscard]] bool hasError() const; + + bool visit(QQmlJS::AST::UiImport *) override; + void endVisit(QQmlJS::AST::UiImport *) override; + + bool visit(QQmlJS::AST::UiPublicMember *) override; + bool visit(QQmlJS::AST::UiObjectDefinition *) override; + + bool visit(QQmlJS::AST::UiObjectInitializer *) override; + void endVisit(QQmlJS::AST::UiObjectInitializer *) override; + + bool visit(QQmlJS::AST::UiObjectBinding *) override; + bool visit(QQmlJS::AST::UiScriptBinding *) override; + bool visit(QQmlJS::AST::UiArrayBinding *) override; + bool visit(QQmlJS::AST::UiArrayMemberList *) override; + bool visit(QQmlJS::AST::UiQualifiedId *) override; + + bool visit(QQmlJS::AST::ThisExpression *) override; + bool visit(QQmlJS::AST::IdentifierExpression *) override; + bool visit(QQmlJS::AST::NullExpression *) override; + bool visit(QQmlJS::AST::TrueLiteral *) override; + bool visit(QQmlJS::AST::FalseLiteral *) override; + bool visit(QQmlJS::AST::NumericLiteral *) override; + bool visit(QQmlJS::AST::StringLiteral *) override; + bool visit(QQmlJS::AST::RegExpLiteral *) override; + bool visit(QQmlJS::AST::ArrayPattern *) override; + + bool visit(QQmlJS::AST::ObjectPattern *) override; + void endVisit(QQmlJS::AST::ObjectPattern *) override; + + bool visit(QQmlJS::AST::PatternElementList *) override; + bool visit(QQmlJS::AST::Elision *) override; + bool visit(QQmlJS::AST::PatternProperty *) override; + bool visit(QQmlJS::AST::ArrayMemberExpression *) override; + bool visit(QQmlJS::AST::FieldMemberExpression *) override; + bool visit(QQmlJS::AST::NewMemberExpression *) override; + bool visit(QQmlJS::AST::NewExpression *) override; + bool visit(QQmlJS::AST::ArgumentList *) override; + bool visit(QQmlJS::AST::PostIncrementExpression *) override; + bool visit(QQmlJS::AST::PostDecrementExpression *) override; + bool visit(QQmlJS::AST::DeleteExpression *) override; + bool visit(QQmlJS::AST::VoidExpression *) override; + bool visit(QQmlJS::AST::TypeOfExpression *) override; + bool visit(QQmlJS::AST::PreIncrementExpression *) override; + bool visit(QQmlJS::AST::PreDecrementExpression *) override; + bool visit(QQmlJS::AST::UnaryPlusExpression *) override; + bool visit(QQmlJS::AST::UnaryMinusExpression *) override; + bool visit(QQmlJS::AST::TildeExpression *) override; + bool visit(QQmlJS::AST::NotExpression *) override; + bool visit(QQmlJS::AST::BinaryExpression *) override; + bool visit(QQmlJS::AST::ConditionalExpression *) override; + bool visit(QQmlJS::AST::Expression *) override; + + bool visit(QQmlJS::AST::Block *) override; + void endVisit(QQmlJS::AST::Block *) override; + + bool visit(QQmlJS::AST::VariableStatement *) override; + bool visit(QQmlJS::AST::VariableDeclarationList *) override; + bool visit(QQmlJS::AST::EmptyStatement *) override; + bool visit(QQmlJS::AST::ExpressionStatement *) override; + bool visit(QQmlJS::AST::IfStatement *) override; + bool visit(QQmlJS::AST::DoWhileStatement *) override; + bool visit(QQmlJS::AST::WhileStatement *) override; + bool visit(QQmlJS::AST::ForStatement *) override; + bool visit(QQmlJS::AST::ForEachStatement *) override; + bool visit(QQmlJS::AST::ContinueStatement *) override; + bool visit(QQmlJS::AST::BreakStatement *) override; + bool visit(QQmlJS::AST::ReturnStatement *) override; + bool visit(QQmlJS::AST::WithStatement *) override; + + bool visit(QQmlJS::AST::CaseBlock *) override; + void endVisit(QQmlJS::AST::CaseBlock *) override; + + bool visit(QQmlJS::AST::SwitchStatement *) override; + bool visit(QQmlJS::AST::CaseClause *) override; + bool visit(QQmlJS::AST::DefaultClause *) override; + bool visit(QQmlJS::AST::LabelledStatement *) override; + bool visit(QQmlJS::AST::ThrowStatement *) override; + bool visit(QQmlJS::AST::TryStatement *) override; + bool visit(QQmlJS::AST::Catch *) override; + bool visit(QQmlJS::AST::Finally *) override; + bool visit(QQmlJS::AST::FunctionDeclaration *) override; + bool visit(QQmlJS::AST::FunctionExpression *) override; + bool visit(QQmlJS::AST::FormalParameterList *) override; + bool visit(QQmlJS::AST::DebuggerStatement *) override; + +protected: + QString protect(const QString &string); + +private: + typedef QHash<QString, QString> StringHash; + void addExtra(quint32 start, quint32 finish); + void addMarkedUpToken(QQmlJS::SourceLocation &location, const QString &text, + const StringHash &attributes = StringHash()); + void addVerbatim(QQmlJS::SourceLocation first, + QQmlJS::SourceLocation last = QQmlJS::SourceLocation()); + QString sourceText(QQmlJS::SourceLocation &location); + void throwRecursionDepthError() final; + + QQmlJS::Engine *m_engine { nullptr }; + QList<ExtraType> m_extraTypes {}; + QList<QQmlJS::SourceLocation> m_extraLocations {}; + QString m_source {}; + QString m_output {}; + quint32 m_cursor {}; + int m_extraIndex {}; + bool m_hasRecursionDepthError { false }; +}; +Q_DECLARE_TYPEINFO(QmlMarkupVisitor::ExtraType, Q_PRIMITIVE_TYPE); + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/qmlpropertynode.cpp b/src/qdoc/qdoc/src/qdoc/qmlpropertynode.cpp new file mode 100644 index 000000000..335b7d870 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/qmlpropertynode.cpp @@ -0,0 +1,175 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "qmlpropertynode.h" + +#include "classnode.h" +#include "propertynode.h" +#include "enumnode.h" + +#include <utility> +#include "qdocdatabase.h" +#include "utilities.h" + +QT_BEGIN_NAMESPACE + +/*! + Constructor for the QML property node. + */ +QmlPropertyNode::QmlPropertyNode(Aggregate *parent, const QString &name, QString type, + bool attached) + : Node(QmlProperty, parent, name), + m_type(std::move(type)), + m_attached(attached) +{ + if (m_type == "alias") + m_isAlias = true; + if (name.startsWith("__")) + setStatus(Internal); +} + +/*! + \fn bool QmlPropertyNode::isReadOnly() const + + Returns \c true if this QML property node is marked as a + read-only property. +*/ + +/*! + \fn const EnumNode *QmlPropertyNode::enumNode() const + + Returns the node representing the C++ enumeration associated + with this property, or \nullptr. +*/ + +/*! + Returns the prefix to use for documentated enumerators from + the associated C++ enum for this property. +*/ +const QString &QmlPropertyNode::enumPrefix() const +{ + return !m_enumNode.second.isEmpty() ? + m_enumNode.second : parent()->name(); +} + +/*! + Locates the node specified by \a path and sets it as the C++ enumeration + associated with this property. + + \a registeredQmlName is used as the prefix in the generated enum value + documentation. + + \note The target EnumNode is searched under the primary tree only. + + Returns \c true on success. +*/ +bool QmlPropertyNode::setEnumNode(const QString &path, const QString ®isteredQmlName) +{ + m_enumNode.first = static_cast<EnumNode*>( + QDocDatabase::qdocDB()->primaryTree()->findNodeByNameAndType(path.split("::"), &Node::isEnumType) + ); + m_enumNode.second = registeredQmlName; + return m_enumNode.first != nullptr; +} + +/*! + Returns \c true if this QML property or attached property is + read-only. If the read-only status is not set explicitly + using the \\readonly command, QDoc attempts to resolve it + from the associated C++ class instantiated by the QML type + that this property belongs to. + + \note Depending on how the QML type is implemented, this + information may not be available to QDoc. If so, add a debug + line but do not treat it as a warning. + */ +bool QmlPropertyNode::isReadOnly() +{ + if (m_readOnly != FlagValueDefault) + return fromFlagValue(m_readOnly, false); + + // Find the parent QML type node + auto *parent{this->parent()}; + while (parent && !(parent->isQmlType())) + parent = parent->parent(); + + bool readonly{false}; + if (auto qcn = static_cast<QmlTypeNode *>(parent); qcn && qcn->classNode()) { + if (auto propertyNode = findCorrespondingCppProperty(); propertyNode) + readonly = !propertyNode->isWritable(); + else + qCDebug(lcQdoc).nospace() + << qPrintable(defLocation().toString()) + << ": Automatic resolution of QML property attributes failed for " + << name() + << " (Q_PROPERTY not found in the C++ class hierarchy known to QDoc. " + << "Likely, the type is replaced with a private implementation.)"; + } + markReadOnly(readonly); + return readonly; +} + +/*! + Returns \c true if this QML property is marked with \required or the + corresponding C++ property uses the REQUIRED keyword. +*/ +bool QmlPropertyNode::isRequired() +{ + if (m_required != FlagValueDefault) + return fromFlagValue(m_required, false); + + PropertyNode *pn = findCorrespondingCppProperty(); + return pn != nullptr && pn->isRequired(); +} + +/*! + Returns a pointer this QML property's corresponding C++ + property, if it has one. + */ +PropertyNode *QmlPropertyNode::findCorrespondingCppProperty() +{ + PropertyNode *pn; + Node *n = parent(); + while (n && !(n->isQmlType())) + n = n->parent(); + if (n) { + auto *qcn = static_cast<QmlTypeNode *>(n); + ClassNode *cn = qcn->classNode(); + if (cn) { + /* + If there is a dot in the property name, first + find the C++ property corresponding to the QML + property group. + */ + QStringList dotSplit = name().split(QChar('.')); + pn = cn->findPropertyNode(dotSplit[0]); + if (pn) { + /* + Now find the C++ property corresponding to + the QML property in the QML property group, + <group>.<property>. + */ + if (dotSplit.size() > 1) { + QStringList path(extractClassName(pn->qualifiedDataType())); + Node *nn = QDocDatabase::qdocDB()->findClassNode(path); + if (nn) { + auto *cn = static_cast<ClassNode *>(nn); + PropertyNode *pn2 = cn->findPropertyNode(dotSplit[1]); + /* + If found, return the C++ property + corresponding to the QML property. + Otherwise, return the C++ property + corresponding to the QML property + group. + */ + return (pn2 ? pn2 : pn); + } + } else + return pn; + } + } + } + return nullptr; +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/qmlpropertynode.h b/src/qdoc/qdoc/src/qdoc/qmlpropertynode.h new file mode 100644 index 000000000..f966949c1 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/qmlpropertynode.h @@ -0,0 +1,72 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef QMLPROPERTYNODE_H +#define QMLPROPERTYNODE_H + +#include "aggregate.h" +#include "node.h" + +#include <QtCore/qglobal.h> +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +class QmlPropertyNode : public Node +{ +public: + QmlPropertyNode(Aggregate *parent, const QString &name, QString type, bool attached); + + void setDataType(const QString &dataType) override { m_type = dataType; } + void setStored(bool stored) { m_stored = toFlagValue(stored); } + void setDefaultValue(const QString &value) { m_defaultValue = value; } + void setRequired() { m_required = toFlagValue(true); } + bool setEnumNode(const QString &path, const QString ®isteredQmlName); + + [[nodiscard]] const QString &dataType() const { return m_type; } + [[nodiscard]] const QString &defaultValue() const { return m_defaultValue; } + [[nodiscard]] bool isStored() const { return fromFlagValue(m_stored, true); } + bool isRequired(); + [[nodiscard]] bool isDefault() const override { return m_isDefault; } + [[nodiscard]] bool isReadOnly() const { return fromFlagValue(m_readOnly, false); } + [[nodiscard]] bool isReadOnly(); + [[nodiscard]] bool isAlias() const override { return m_isAlias; } + [[nodiscard]] bool isAttached() const override { return m_attached; } + [[nodiscard]] QString qmlTypeName() const override { return parent()->qmlTypeName(); } + [[nodiscard]] QString logicalModuleName() const override + { + return parent()->logicalModuleName(); + } + [[nodiscard]] QString logicalModuleVersion() const override + { + return parent()->logicalModuleVersion(); + } + [[nodiscard]] QString logicalModuleIdentifier() const override + { + return parent()->logicalModuleIdentifier(); + } + [[nodiscard]] QString element() const override { return parent()->name(); } + [[nodiscard]] const EnumNode *enumNode() const { return m_enumNode.first; } + [[nodiscard]] const QString &enumPrefix() const; + + void markDefault() override { m_isDefault = true; } + void markReadOnly(bool flag) override { m_readOnly = toFlagValue(flag); } + +private: + PropertyNode *findCorrespondingCppProperty(); + +private: + QString m_type {}; + QString m_defaultValue {}; + FlagValue m_stored { FlagValueDefault }; + bool m_isAlias { false }; + bool m_isDefault { false }; + bool m_attached {}; + FlagValue m_readOnly { FlagValueDefault }; + FlagValue m_required { FlagValueDefault }; + std::pair<EnumNode *, QString> m_enumNode { nullptr, {} }; +}; + +QT_END_NAMESPACE + +#endif // QMLPROPERTYNODE_H diff --git a/src/qdoc/qdoc/src/qdoc/qmltypenode.cpp b/src/qdoc/qdoc/src/qdoc/qmltypenode.cpp new file mode 100644 index 000000000..4285f9b6e --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/qmltypenode.cpp @@ -0,0 +1,153 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "qmltypenode.h" +#include "collectionnode.h" +#include "qdocdatabase.h" + +#include <QtCore/qdebug.h> + +QT_BEGIN_NAMESPACE + +QMultiMap<const Node *, Node *> QmlTypeNode::s_inheritedBy; + +/*! + Constructs a Qml type. + + The new node has the given \a parent, name \a name, and a specific node + \a type. Valid types are Node::QmlType and Node::QmlValueType. + */ +QmlTypeNode::QmlTypeNode(Aggregate *parent, const QString &name, Node::NodeType type) + : Aggregate(type, parent, name) +{ + Q_ASSERT(type == Node::QmlType || type == Node::QmlValueType); + setTitle(name); +} + +/*! + Clear the static maps so that subsequent runs don't try to use + contents from a previous run. + */ +void QmlTypeNode::terminate() +{ + s_inheritedBy.clear(); +} + +/*! + Record the fact that QML class \a base is inherited by + QML class \a sub. + */ +void QmlTypeNode::addInheritedBy(const Node *base, Node *sub) +{ + if (sub->isInternal()) + return; + if (!s_inheritedBy.contains(base, sub)) + s_inheritedBy.insert(base, sub); +} + +/*! + Loads the list \a subs with the nodes of all the subclasses of \a base. + */ +void QmlTypeNode::subclasses(const Node *base, NodeList &subs) +{ + subs.clear(); + if (s_inheritedBy.count(base) > 0) { + subs = s_inheritedBy.values(base); + } +} + +/*! + If this QML type node has a base type node, + return the fully qualified name of that QML + type, i.e. <QML-module-name>::<QML-type-name>. + */ +QString QmlTypeNode::qmlFullBaseName() const +{ + QString result; + if (m_qmlBaseNode) { + result = m_qmlBaseNode->logicalModuleName() + "::" + m_qmlBaseNode->name(); + } + return result; +} + +/*! + If the QML type's QML module pointer is set, return the QML + module name from the QML module node. Otherwise, return the + empty string. + */ +QString QmlTypeNode::logicalModuleName() const +{ + return (m_logicalModule ? m_logicalModule->logicalModuleName() : QString()); +} + +/*! + If the QML type's QML module pointer is set, return the QML + module version from the QML module node. Otherwise, return + the empty string. + */ +QString QmlTypeNode::logicalModuleVersion() const +{ + return (m_logicalModule ? m_logicalModule->logicalModuleVersion() : QString()); +} + +/*! + If the QML type's QML module pointer is set, return the QML + module identifier from the QML module node. Otherwise, return + the empty string. + */ +QString QmlTypeNode::logicalModuleIdentifier() const +{ + return (m_logicalModule ? m_logicalModule->logicalModuleIdentifier() : QString()); +} + +/*! + Returns true if this QML type inherits \a type. + */ +bool QmlTypeNode::inherits(Aggregate *type) +{ + QmlTypeNode *qtn = qmlBaseNode(); + while (qtn != nullptr) { + if (qtn == type) + return true; + qtn = qtn->qmlBaseNode(); + } + return false; +} + +/*! + Recursively resolves the base node for this QML type when only the name of + the base type is known. + + \a previousSearches is used for speeding up the process. +*/ +void QmlTypeNode::resolveInheritance(NodeMap &previousSearches) +{ + if (m_qmlBaseNode || m_qmlBaseName.isEmpty()) + return; + + auto *base = static_cast<QmlTypeNode *>(previousSearches.value(m_qmlBaseName)); + if (!previousSearches.contains(m_qmlBaseName)) { + for (const auto &imp : std::as_const(m_importList)) { + base = QDocDatabase::qdocDB()->findQmlType(imp, m_qmlBaseName); + if (base) + break; + } + if (!base) { + if (m_qmlBaseName.contains(':')) + base = QDocDatabase::qdocDB()->findQmlType(m_qmlBaseName); + else + base = QDocDatabase::qdocDB()->findQmlType(QString(), m_qmlBaseName); + } + previousSearches.insert(m_qmlBaseName, base); + } + + if (base && base != this) { + m_qmlBaseNode = base; + QmlTypeNode::addInheritedBy(base, this); + // Base types read from the index need resolving as they only have the name set + if (base->isIndexNode()) + base->resolveInheritance(previousSearches); + } +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/qmltypenode.h b/src/qdoc/qdoc/src/qdoc/qmltypenode.h new file mode 100644 index 000000000..d7cd5ff2b --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/qmltypenode.h @@ -0,0 +1,65 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef QMLTYPENODE_H +#define QMLTYPENODE_H + +#include "importrec.h" +#include "aggregate.h" + +#include <QtCore/qglobal.h> +#include <QtCore/qlist.h> +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +class ClassNode; +class CollectionNode; + +typedef QList<ImportRec> ImportList; + +class QmlTypeNode : public Aggregate +{ +public: + QmlTypeNode(Aggregate *parent, const QString &name, Node::NodeType type); + [[nodiscard]] bool isFirstClassAggregate() const override { return true; } + ClassNode *classNode() override { return m_classNode; } + void setClassNode(ClassNode *cn) override { m_classNode = cn; } + [[nodiscard]] bool isAbstract() const override { return m_abstract; } + [[nodiscard]] bool isWrapper() const override { return m_wrapper; } + void setAbstract(bool b) override { m_abstract = b; } + void setWrapper() override { m_wrapper = true; } + [[nodiscard]] bool isInternal() const override { return (status() == Internal); } + [[nodiscard]] QString qmlFullBaseName() const override; + [[nodiscard]] QString logicalModuleName() const override; + [[nodiscard]] QString logicalModuleVersion() const override; + [[nodiscard]] QString logicalModuleIdentifier() const override; + [[nodiscard]] CollectionNode *logicalModule() const override { return m_logicalModule; } + void setQmlModule(CollectionNode *t) override { m_logicalModule = t; } + + void setImportList(const ImportList &il) { m_importList = il; } + [[nodiscard]] const QString &qmlBaseName() const { return m_qmlBaseName; } + void setQmlBaseName(const QString &name) { m_qmlBaseName = name; } + [[nodiscard]] QmlTypeNode *qmlBaseNode() const override { return m_qmlBaseNode; } + void resolveInheritance(NodeMap &previousSearches); + static void addInheritedBy(const Node *base, Node *sub); + static void subclasses(const Node *base, NodeList &subs); + static void terminate(); + bool inherits(Aggregate *type); + +public: + static QMultiMap<const Node *, Node *> s_inheritedBy; + +private: + bool m_abstract { false }; + bool m_wrapper { false }; + ClassNode *m_classNode { nullptr }; + QString m_qmlBaseName {}; + CollectionNode *m_logicalModule { nullptr }; + QmlTypeNode *m_qmlBaseNode { nullptr }; + ImportList m_importList {}; +}; + +QT_END_NAMESPACE + +#endif // QMLTYPENODE_H diff --git a/src/qdoc/qdoc/src/qdoc/qmlvisitor.cpp b/src/qdoc/qdoc/src/qdoc/qmlvisitor.cpp new file mode 100644 index 000000000..d6ecf1986 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/qmlvisitor.cpp @@ -0,0 +1,729 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "qmlvisitor.h" + +#include "aggregate.h" +#include "codechunk.h" +#include "codeparser.h" +#include "functionnode.h" +#include "node.h" +#include "qdocdatabase.h" +#include "qmlpropertynode.h" +#include "tokenizer.h" +#include "utilities.h" + +#include <QtCore/qdebug.h> +#include <QtCore/qfileinfo.h> +#include <QtCore/qglobal.h> + +#include <private/qqmljsast_p.h> +#include <private/qqmljsengine_p.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +/*! + The constructor stores all the parameters in local data members. + */ +QmlDocVisitor::QmlDocVisitor(const QString &filePath, const QString &code, QQmlJS::Engine *engine, + const QSet<QString> &commands, const QSet<QString> &topics) + : m_nestingLevel(0) +{ + m_lastEndOffset = 0; + this->m_filePath = filePath; + this->m_name = QFileInfo(filePath).baseName(); + m_document = code; + this->m_engine = engine; + this->m_commands = commands; + this->m_topics = topics; + m_current = QDocDatabase::qdocDB()->primaryTreeRoot(); +} + +/*! + Returns the location of the nearest comment above the \a offset. + */ +QQmlJS::SourceLocation QmlDocVisitor::precedingComment(quint32 offset) const +{ + const auto comments = m_engine->comments(); + for (auto it = comments.rbegin(); it != comments.rend(); ++it) { + QQmlJS::SourceLocation loc = *it; + + if (loc.begin() <= m_lastEndOffset) { + // Return if we reach the end of the preceding structure. + break; + } else if (m_usedComments.contains(loc.begin())) { + // Return if we encounter a previously used comment. + break; + } else if (loc.begin() > m_lastEndOffset && loc.end() < offset) { + // Only examine multiline comments in order to avoid snippet markers. + if (m_document.at(loc.offset - 1) == QLatin1Char('*')) { + QString comment = m_document.mid(loc.offset, loc.length); + if (comment.startsWith(QLatin1Char('!')) || comment.startsWith(QLatin1Char('*'))) { + return loc; + } + } + } + } + + return QQmlJS::SourceLocation(); +} + +class QmlSignatureParser +{ +public: + QmlSignatureParser(FunctionNode *func, const QString &signature, const Location &loc); + void readToken() { tok_ = tokenizer_->getToken(); } + QString lexeme() { return tokenizer_->lexeme(); } + QString previousLexeme() { return tokenizer_->previousLexeme(); } + + bool match(int target); + bool matchTypeAndName(CodeChunk *type, QString *var); + bool matchParameter(); + bool matchFunctionDecl(); + +private: + QString signature_; + QStringList names_; + Tokenizer *tokenizer_; + int tok_; + FunctionNode *func_; + const Location &location_; +}; + +/*! + Finds the nearest unused qdoc comment above the QML entity + represented by a \a node and processes the qdoc commands + in that comment. The processed documentation is stored in + \a node. + + If \a node is a \c nullptr and there is a valid comment block, + the QML module identifier (\inqmlmodule argument) is used + for searching an existing QML type node. If an existing node + is not found, constructs a new QmlTypeNode instance. + + Returns a pointer to the QmlTypeNode instance if one was + found or constructed. Otherwise, returns a pointer to the \a + node that was passed as an argument. + */ +Node *QmlDocVisitor::applyDocumentation(QQmlJS::SourceLocation location, Node *node) +{ + QQmlJS::SourceLocation loc = precedingComment(location.begin()); + Location comment_loc(m_filePath); + + // No preceding comment; construct a new QML type if + // needed. + if (!loc.isValid()) { + if (!node) + node = new QmlTypeNode(m_current, m_name, Node::QmlType); + comment_loc.setLineNo(location.startLine); + node->setLocation(comment_loc); + return node; + } + + QString source = m_document.mid(loc.offset + 1, loc.length - 1); + comment_loc.setLineNo(loc.startLine); + comment_loc.setColumnNo(loc.startColumn); + + Doc doc(comment_loc, comment_loc, source, m_commands, m_topics); + const TopicList &topicsUsed = doc.topicsUsed(); + NodeList nodes; + if (!node) { + QString qmid; + if (auto args = doc.metaCommandArgs(COMMAND_INQMLMODULE); !args.isEmpty()) + qmid = args.first().first; + node = QDocDatabase::qdocDB()->findQmlTypeInPrimaryTree(qmid, m_name); + if (!node) { + node = new QmlTypeNode(m_current, m_name, Node::QmlType); + node->setLocation(comment_loc); + } + } + + auto *parent{node->parent()}; + node->setDoc(doc); + nodes.append(node); + if (!topicsUsed.empty()) { + for (int i = 0; i < topicsUsed.size(); ++i) { + QString topic = topicsUsed.at(i).m_topic; + QString args = topicsUsed.at(i).m_args; + if (topic.endsWith(QLatin1String("property"))) { + auto *qmlProperty = static_cast<QmlPropertyNode *>(node); + QmlPropArgs qpa; + if (splitQmlPropertyArg(doc, args, qpa)) { + if (qpa.m_name == node->name()) { + if (qmlProperty->isAlias()) + qmlProperty->setDataType(qpa.m_type); + } else { + bool isAttached = topic.contains(QLatin1String("attached")); + QmlPropertyNode *n = parent->hasQmlProperty(qpa.m_name, isAttached); + if (n == nullptr) + n = new QmlPropertyNode(parent, qpa.m_name, qpa.m_type, isAttached); + n->setLocation(doc.location()); + n->setDoc(doc); + // Use the const-overload of QmlPropertyNode::isReadOnly() as there's + // no associated C++ property to resolve the read-only status from + n->markReadOnly(const_cast<const QmlPropertyNode *>(qmlProperty)->isReadOnly() + && !isAttached); + if (qmlProperty->isDefault()) + n->markDefault(); + nodes.append(n); + } + } else + qCDebug(lcQdoc) << "Failed to parse QML property:" << topic << args; + } else if (topic.endsWith(QLatin1String("method")) || topic == COMMAND_QMLSIGNAL) { + if (node->isFunction()) { + auto *fn = static_cast<FunctionNode *>(node); + QmlSignatureParser qsp(fn, args, doc.location()); + } + } + } + } + for (const auto &node : nodes) + applyMetacommands(loc, node, doc); + + m_usedComments.insert(loc.offset); + return node; +} + +QmlSignatureParser::QmlSignatureParser(FunctionNode *func, const QString &signature, + const Location &loc) + : signature_(signature), func_(func), location_(loc) +{ + QByteArray latin1 = signature.toLatin1(); + Tokenizer stringTokenizer(location_, latin1); + stringTokenizer.setParsingFnOrMacro(true); + tokenizer_ = &stringTokenizer; + readToken(); + matchFunctionDecl(); +} + +/*! + If the current token matches \a target, read the next + token and return true. Otherwise, don't read the next + token, and return false. + */ +bool QmlSignatureParser::match(int target) +{ + if (tok_ == target) { + readToken(); + return true; + } + return false; +} + +/*! + Parse a QML data type into \a type and an optional + variable name into \a var. + */ +bool QmlSignatureParser::matchTypeAndName(CodeChunk *type, QString *var) +{ + /* + This code is really hard to follow... sorry. The loop is there to match + Alpha::Beta::Gamma::...::Omega. + */ + for (;;) { + bool virgin = true; + + if (tok_ != Tok_Ident) { + while (match(Tok_signed) || match(Tok_unsigned) || match(Tok_short) || match(Tok_long) + || match(Tok_int64)) { + type->append(previousLexeme()); + virgin = false; + } + } + + if (virgin) { + if (match(Tok_Ident)) { + type->append(previousLexeme()); + } else if (match(Tok_void) || match(Tok_int) || match(Tok_char) || match(Tok_double) + || match(Tok_Ellipsis)) + type->append(previousLexeme()); + else + return false; + } else if (match(Tok_int) || match(Tok_char) || match(Tok_double)) { + type->append(previousLexeme()); + } + + if (match(Tok_Gulbrandsen)) + type->append(previousLexeme()); + else + break; + } + + while (match(Tok_Ampersand) || match(Tok_Aster) || match(Tok_const) || match(Tok_Caret)) + type->append(previousLexeme()); + + /* + The usual case: Look for an optional identifier, then for + some array brackets. + */ + type->appendHotspot(); + + if ((var != nullptr) && match(Tok_Ident)) + *var = previousLexeme(); + + if (tok_ == Tok_LeftBracket) { + int bracketDepth0 = tokenizer_->bracketDepth(); + while ((tokenizer_->bracketDepth() >= bracketDepth0 && tok_ != Tok_Eoi) + || tok_ == Tok_RightBracket) { + type->append(lexeme()); + readToken(); + } + } + return true; +} + +bool QmlSignatureParser::matchParameter() +{ + QString name; + CodeChunk type; + CodeChunk defaultValue; + + bool result = matchTypeAndName(&type, &name); + if (name.isEmpty()) { + name = type.toString(); + type.clear(); + } + + if (!result) + return false; + if (match(Tok_Equal)) { + int parenDepth0 = tokenizer_->parenDepth(); + while (tokenizer_->parenDepth() >= parenDepth0 + && (tok_ != Tok_Comma || tokenizer_->parenDepth() > parenDepth0) + && tok_ != Tok_Eoi) { + defaultValue.append(lexeme()); + readToken(); + } + } + func_->parameters().append(type.toString(), name, defaultValue.toString()); + return true; +} + +bool QmlSignatureParser::matchFunctionDecl() +{ + CodeChunk returnType; + + qsizetype firstBlank = signature_.indexOf(QChar(' ')); + qsizetype leftParen = signature_.indexOf(QChar('(')); + if ((firstBlank > 0) && (leftParen - firstBlank) > 1) { + if (!matchTypeAndName(&returnType, nullptr)) + return false; + } + + while (match(Tok_Ident)) { + names_.append(previousLexeme()); + if (!match(Tok_Gulbrandsen)) { + previousLexeme(); + names_.pop_back(); + break; + } + } + + if (tok_ != Tok_LeftParen) + return false; + /* + Parsing the parameters should be moved into class Parameters, + but it can wait. mws 14/12/2018 + */ + readToken(); + + func_->setLocation(location_); + func_->setReturnType(returnType.toString()); + + if (tok_ != Tok_RightParen) { + func_->parameters().clear(); + do { + if (!matchParameter()) + return false; + } while (match(Tok_Comma)); + } + if (!match(Tok_RightParen)) + return false; + return true; +} + +/*! + A QML property argument has the form... + + <type> <component>::<name> + <type> <QML-module>::<component>::<name> + + This function splits the argument into one of those + two forms. The three part form is the old form, which + was used before the creation of QtQuick 2 and Qt + Components. A <QML-module> is the QML equivalent of a + C++ namespace. So this function splits \a arg on "::" + and stores the parts in the \e {type}, \e {module}, + \e {component}, and \a {name}, fields of \a qpa. If it + is successful, it returns \c true. If not enough parts + are found, a qdoc warning is emitted and false is + returned. + */ +bool QmlDocVisitor::splitQmlPropertyArg(const Doc &doc, const QString &arg, QmlPropArgs &qpa) +{ + qpa.clear(); + QStringList blankSplit = arg.split(QLatin1Char(' ')); + if (blankSplit.size() > 1) { + qpa.m_type = blankSplit[0]; + QStringList colonSplit(blankSplit[1].split("::")); + if (colonSplit.size() == 3) { + qpa.m_module = colonSplit[0]; + qpa.m_component = colonSplit[1]; + qpa.m_name = colonSplit[2]; + return true; + } else if (colonSplit.size() == 2) { + qpa.m_component = colonSplit[0]; + qpa.m_name = colonSplit[1]; + return true; + } else if (colonSplit.size() == 1) { + qpa.m_name = colonSplit[0]; + return true; + } + doc.location().warning( + QStringLiteral("Unrecognizable QML module/component qualifier for %1.").arg(arg)); + } else { + doc.location().warning(QStringLiteral("Missing property type for %1.").arg(arg)); + } + return false; +} + +/*! + Applies the metacommands found in the comment. + */ +void QmlDocVisitor::applyMetacommands(QQmlJS::SourceLocation, Node *node, Doc &doc) +{ + QDocDatabase *qdb = QDocDatabase::qdocDB(); + QSet<QString> metacommands = doc.metaCommandsUsed(); + if (metacommands.size() > 0) { + metacommands.subtract(m_topics); + for (const auto &command : std::as_const(metacommands)) { + const ArgList args = doc.metaCommandArgs(command); + if ((command == COMMAND_QMLABSTRACT) || (command == COMMAND_ABSTRACT)) { + if (node->isQmlType()) { + node->setAbstract(true); + } + } else if (command == COMMAND_DEPRECATED) { + node->setDeprecated(args[0].second); + } else if (command == COMMAND_INQMLMODULE) { + qdb->addToQmlModule(args[0].first, node); + } else if (command == COMMAND_QMLINHERITS) { + if (node->name() == args[0].first) + doc.location().warning( + QStringLiteral("%1 tries to inherit itself").arg(args[0].first)); + else if (node->isQmlType()) { + auto *qmlType = static_cast<QmlTypeNode *>(node); + qmlType->setQmlBaseName(args[0].first); + } + } else if (command == COMMAND_DEFAULT) { + if (!node->isQmlProperty()) { + doc.location().warning(QStringLiteral("Ignored '\\%1', applies only to '\\%2'") + .arg(command, COMMAND_QMLPROPERTY)); + } else if (args.isEmpty() || args[0].first.isEmpty()) { + doc.location().warning(QStringLiteral("Expected an argument for '\\%1' (maybe you meant '\\%2'?)") + .arg(command, COMMAND_QMLDEFAULT)); + } else { + static_cast<QmlPropertyNode *>(node)->setDefaultValue(args[0].first); + } + } else if (command == COMMAND_QMLDEFAULT) { + node->markDefault(); + } else if (command == COMMAND_QMLENUMERATORSFROM) { + if (!node->isQmlProperty()) { + doc.location().warning("Ignored '\\%1', applies only to '\\%2'"_L1 + .arg(command, COMMAND_QMLPROPERTY)); + } else if (!static_cast<QmlPropertyNode*>(node)->setEnumNode(args[0].first, args[0].second)) { + doc.location().warning("Failed to find C++ enumeration '%2' passed to \\%1"_L1 + .arg(command, args[0].first), "Use \\value commands instead"_L1); + } + } else if (command == COMMAND_QMLREADONLY) { + node->markReadOnly(1); + } else if (command == COMMAND_QMLREQUIRED) { + if (node->isQmlProperty()) + static_cast<QmlPropertyNode *>(node)->setRequired(); + } else if ((command == COMMAND_INGROUP) && !args.isEmpty()) { + for (const auto &argument : args) + QDocDatabase::qdocDB()->addToGroup(argument.first, node); + } else if (command == COMMAND_INTERNAL) { + node->setStatus(Node::Internal); + } else if (command == COMMAND_OBSOLETE) { + node->setStatus(Node::Deprecated); + } else if (command == COMMAND_PRELIMINARY) { + node->setStatus(Node::Preliminary); + } else if (command == COMMAND_SINCE) { + QString arg = args[0].first; //.join(' '); + node->setSince(arg); + } else if (command == COMMAND_WRAPPER) { + node->setWrapper(); + } else { + doc.location().warning( + QStringLiteral("The \\%1 command is ignored in QML files").arg(command)); + } + } + } +} + +/*! + Reconstruct the qualified \a id using dot notation + and return the fully qualified string. + */ +QString QmlDocVisitor::getFullyQualifiedId(QQmlJS::AST::UiQualifiedId *id) +{ + QString result; + if (id) { + result = id->name.toString(); + id = id->next; + while (id != nullptr) { + result += QChar('.') + id->name.toString(); + id = id->next; + } + } + return result; +} + +/*! + Begin the visit of the object \a definition, recording it in the + qdoc database. Increment the object nesting level, which is used + to test whether we are at the public API level. The public level + is level 1. + + Defers the construction of a QmlTypeNode instance to + applyDocumentation(), by passing \c nullptr as the second + argument. + */ +bool QmlDocVisitor::visit(QQmlJS::AST::UiObjectDefinition *definition) +{ + QString type = getFullyQualifiedId(definition->qualifiedTypeNameId); + m_nestingLevel++; + if (m_current->isNamespace()) { + auto component = applyDocumentation(definition->firstSourceLocation(), nullptr); + Q_ASSERT(component); + auto *qmlTypeNode = static_cast<QmlTypeNode *>(component); + if (!component->doc().isEmpty()) + qmlTypeNode->setQmlBaseName(type); + qmlTypeNode->setTitle(m_name); + qmlTypeNode->setImportList(m_importList); + m_importList.clear(); + m_current = qmlTypeNode; + } + + return true; +} + +/*! + End the visit of the object \a definition. In particular, + decrement the object nesting level, which is used to test + whether we are at the public API level. The public API + level is level 1. It won't decrement below 0. + */ +void QmlDocVisitor::endVisit(QQmlJS::AST::UiObjectDefinition *definition) +{ + if (m_nestingLevel > 0) { + --m_nestingLevel; + } + m_lastEndOffset = definition->lastSourceLocation().end(); +} + +bool QmlDocVisitor::visit(QQmlJS::AST::UiImport *import) +{ + QString name = m_document.mid(import->fileNameToken.offset, import->fileNameToken.length); + if (name[0] == '\"') + name = name.mid(1, name.size() - 2); + QString version; + if (import->version) { + const auto start = import->version->firstSourceLocation().begin(); + const auto end = import->version->lastSourceLocation().end(); + version = m_document.mid(start, end - start); + } + QString importUri = getFullyQualifiedId(import->importUri); + m_importList.append(ImportRec(name, version, importUri)); + + return true; +} + +void QmlDocVisitor::endVisit(QQmlJS::AST::UiImport *definition) +{ + m_lastEndOffset = definition->lastSourceLocation().end(); +} + +bool QmlDocVisitor::visit(QQmlJS::AST::UiObjectBinding *) +{ + ++m_nestingLevel; + return true; +} + +void QmlDocVisitor::endVisit(QQmlJS::AST::UiObjectBinding *) +{ + --m_nestingLevel; +} + +bool QmlDocVisitor::visit(QQmlJS::AST::UiArrayBinding *) +{ + return true; +} + +void QmlDocVisitor::endVisit(QQmlJS::AST::UiArrayBinding *) {} + +static QString qualifiedIdToString(QQmlJS::AST::UiQualifiedId *node) +{ + QString s; + + for (QQmlJS::AST::UiQualifiedId *it = node; it; it = it->next) { + s.append(it->name); + + if (it->next) + s.append(QLatin1Char('.')); + } + + return s; +} + +/*! + Visits the public \a member declaration, which can be a + signal or a property. It is a custom signal or property. + Only visit the \a member if the nestingLevel is 1. + */ +bool QmlDocVisitor::visit(QQmlJS::AST::UiPublicMember *member) +{ + if (m_nestingLevel > 1) { + return true; + } + switch (member->type) { + case QQmlJS::AST::UiPublicMember::Signal: { + if (m_current->isQmlType()) { + auto *qmlType = static_cast<QmlTypeNode *>(m_current); + if (qmlType) { + FunctionNode::Metaness metaness = FunctionNode::QmlSignal; + QString name = member->name.toString(); + auto *newSignal = new FunctionNode(metaness, m_current, name); + Parameters ¶meters = newSignal->parameters(); + for (QQmlJS::AST::UiParameterList *it = member->parameters; it; it = it->next) { + const QString type = it->type ? it->type->toString() : QString(); + if (!type.isEmpty() && !it->name.isEmpty()) + parameters.append(type, it->name.toString()); + } + applyDocumentation(member->firstSourceLocation(), newSignal); + } + } + break; + } + case QQmlJS::AST::UiPublicMember::Property: { + QString type = qualifiedIdToString(member->memberType); + if (m_current->isQmlType()) { + auto *qmlType = static_cast<QmlTypeNode *>(m_current); + if (qmlType) { + QString name = member->name.toString(); + QmlPropertyNode *qmlPropNode = qmlType->hasQmlProperty(name); + if (qmlPropNode == nullptr) + qmlPropNode = new QmlPropertyNode(qmlType, name, type, false); + qmlPropNode->markReadOnly(member->isReadonly()); + if (member->isDefaultMember()) + qmlPropNode->markDefault(); + if (member->requiredToken().isValid()) + qmlPropNode->setRequired(); + applyDocumentation(member->firstSourceLocation(), qmlPropNode); + } + } + break; + } + default: + return false; + } + + return true; +} + +/*! + End the visit of the \a member. + */ +void QmlDocVisitor::endVisit(QQmlJS::AST::UiPublicMember *member) +{ + m_lastEndOffset = member->lastSourceLocation().end(); +} + +bool QmlDocVisitor::visit(QQmlJS::AST::IdentifierPropertyName *) +{ + return true; +} + +/*! + Begin the visit of the function declaration \a fd, but only + if the nesting level is 1. + */ +bool QmlDocVisitor::visit(QQmlJS::AST::FunctionDeclaration *fd) +{ + if (m_nestingLevel <= 1) { + FunctionNode::Metaness metaness = FunctionNode::QmlMethod; + if (!m_current->isQmlType()) + return true; + QString name = fd->name.toString(); + auto *method = new FunctionNode(metaness, m_current, name); + Parameters ¶meters = method->parameters(); + QQmlJS::AST::FormalParameterList *formals = fd->formals; + if (formals) { + QQmlJS::AST::FormalParameterList *fp = formals; + do { + QString defaultValue; + auto initializer = fp->element->initializer; + if (initializer) { + auto loc = initializer->firstSourceLocation(); + defaultValue = m_document.mid(loc.begin(), loc.length); + } + parameters.append(QString(), fp->element->bindingIdentifier.toString(), + defaultValue); + fp = fp->next; + } while (fp && fp != formals); + } + applyDocumentation(fd->firstSourceLocation(), method); + } + return true; +} + +/*! + End the visit of the function declaration, \a fd. + */ +void QmlDocVisitor::endVisit(QQmlJS::AST::FunctionDeclaration *fd) +{ + m_lastEndOffset = fd->lastSourceLocation().end(); +} + +/*! + Begin the visit of the signal handler declaration \a sb, but only + if the nesting level is 1. + + This visit is now deprecated. It has been decided to document + public signals. If a signal handler must be discussed in the + documentation, that discussion must take place in the comment + for the signal. + */ +bool QmlDocVisitor::visit(QQmlJS::AST::UiScriptBinding *) +{ + return true; +} + +void QmlDocVisitor::endVisit(QQmlJS::AST::UiScriptBinding *sb) +{ + m_lastEndOffset = sb->lastSourceLocation().end(); +} + +bool QmlDocVisitor::visit(QQmlJS::AST::UiQualifiedId *) +{ + return true; +} + +void QmlDocVisitor::endVisit(QQmlJS::AST::UiQualifiedId *) +{ + // nothing. +} + +void QmlDocVisitor::throwRecursionDepthError() +{ + hasRecursionDepthError = true; +} + +bool QmlDocVisitor::hasError() const +{ + return hasRecursionDepthError; +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/qmlvisitor.h b/src/qdoc/qdoc/src/qdoc/qmlvisitor.h new file mode 100644 index 000000000..201dc0e61 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/qmlvisitor.h @@ -0,0 +1,93 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef QMLVISITOR_H +#define QMLVISITOR_H + +#include "node.h" +#include "qmltypenode.h" + +#include <QtCore/qstring.h> + +#include <private/qqmljsastvisitor_p.h> +#include <private/qqmljsengine_p.h> + +QT_BEGIN_NAMESPACE + +class Aggregate; + +struct QmlPropArgs +{ + QString m_type {}; + QString m_module {}; + QString m_component {}; + QString m_name; + + void clear() + { + m_type.clear(); + m_module.clear(); + m_component.clear(); + m_name.clear(); + } +}; + +class QmlDocVisitor : public QQmlJS::AST::Visitor +{ +public: + QmlDocVisitor(const QString &filePath, const QString &code, QQmlJS::Engine *engine, + const QSet<QString> &commands, const QSet<QString> &topics); + ~QmlDocVisitor() override = default; + + bool visit(QQmlJS::AST::UiImport *import) override; + void endVisit(QQmlJS::AST::UiImport *definition) override; + + bool visit(QQmlJS::AST::UiObjectDefinition *definition) override; + void endVisit(QQmlJS::AST::UiObjectDefinition *definition) override; + + bool visit(QQmlJS::AST::UiPublicMember *member) override; + void endVisit(QQmlJS::AST::UiPublicMember *definition) override; + + bool visit(QQmlJS::AST::UiObjectBinding *) override; + void endVisit(QQmlJS::AST::UiObjectBinding *) override; + void endVisit(QQmlJS::AST::UiArrayBinding *) override; + bool visit(QQmlJS::AST::UiArrayBinding *) override; + + bool visit(QQmlJS::AST::IdentifierPropertyName *idproperty) override; + + bool visit(QQmlJS::AST::FunctionDeclaration *) override; + void endVisit(QQmlJS::AST::FunctionDeclaration *) override; + + bool visit(QQmlJS::AST::UiScriptBinding *) override; + void endVisit(QQmlJS::AST::UiScriptBinding *) override; + + bool visit(QQmlJS::AST::UiQualifiedId *) override; + void endVisit(QQmlJS::AST::UiQualifiedId *) override; + + void throwRecursionDepthError() final; + [[nodiscard]] bool hasError() const; + +private: + QString getFullyQualifiedId(QQmlJS::AST::UiQualifiedId *id); + [[nodiscard]] QQmlJS::SourceLocation precedingComment(quint32 offset) const; + Node *applyDocumentation(QQmlJS::SourceLocation location, Node *node); + void applyMetacommands(QQmlJS::SourceLocation location, Node *node, Doc &doc); + bool splitQmlPropertyArg(const Doc &doc, const QString &arg, QmlPropArgs &qpa); + + QQmlJS::Engine *m_engine { nullptr }; + quint32 m_lastEndOffset {}; + quint32 m_nestingLevel {}; + QString m_filePath {}; + QString m_name {}; + QString m_document {}; + ImportList m_importList {}; + QSet<QString> m_commands {}; + QSet<QString> m_topics {}; + QSet<quint32> m_usedComments {}; + Aggregate *m_current { nullptr }; + bool hasRecursionDepthError { false }; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/quoter.cpp b/src/qdoc/qdoc/src/qdoc/quoter.cpp new file mode 100644 index 000000000..37799a9e9 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/quoter.cpp @@ -0,0 +1,338 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "quoter.h" + +#include <QtCore/qdebug.h> +#include <QtCore/qfileinfo.h> +#include <QtCore/qregularexpression.h> + +QT_BEGIN_NAMESPACE + +QHash<QString, QString> Quoter::s_commentHash; + +static void replaceMultipleNewlines(QString &s) +{ + const qsizetype n = s.size(); + bool slurping = false; + int j = -1; + const QChar newLine = QLatin1Char('\n'); + QChar *d = s.data(); + for (int i = 0; i != n; ++i) { + const QChar c = d[i]; + bool hit = (c == newLine); + if (slurping && hit) + continue; + d[++j] = c; + slurping = hit; + } + s.resize(++j); +} + +// This is equivalent to line.split( QRegularExpression("\n(?!\n|$)") ) but much faster +QStringList Quoter::splitLines(const QString &line) +{ + QStringList result; + qsizetype i = line.size(); + while (true) { + qsizetype j = i - 1; + while (j >= 0 && line.at(j) == QLatin1Char('\n')) + --j; + while (j >= 0 && line.at(j) != QLatin1Char('\n')) + --j; + result.prepend(line.mid(j + 1, i - j - 1)); + if (j < 0) + break; + i = j; + } + return result; +} + +/* + Transforms 'int x = 3 + 4' into 'int x=3+4'. A white space is kept + between 'int' and 'x' because it is meaningful in C++. +*/ +static void trimWhiteSpace(QString &str) +{ + enum { Normal, MetAlnum, MetSpace } state = Normal; + const qsizetype n = str.size(); + + int j = -1; + QChar *d = str.data(); + for (int i = 0; i != n; ++i) { + const QChar c = d[i]; + if (c.isLetterOrNumber()) { + if (state == Normal) { + state = MetAlnum; + } else { + if (state == MetSpace) + str[++j] = c; + state = Normal; + } + str[++j] = c; + } else if (c.isSpace()) { + if (state == MetAlnum) + state = MetSpace; + } else { + state = Normal; + str[++j] = c; + } + } + str.resize(++j); +} + +Quoter::Quoter() : m_silent(false) +{ + /* We're going to hard code these delimiters: + * C++, Qt, Qt Script, Java: + //! [<id>] + * .pro, .py, CMake files: + #! [<id>] + * .html, .qrc, .ui, .xq, .xml files: + <!-- [<id>] --> + */ + if (s_commentHash.empty()) { + s_commentHash["pro"] = "#!"; + s_commentHash["py"] = "#!"; + s_commentHash["cmake"] = "#!"; + s_commentHash["html"] = "<!--"; + s_commentHash["qrc"] = "<!--"; + s_commentHash["ui"] = "<!--"; + s_commentHash["xml"] = "<!--"; + s_commentHash["xq"] = "<!--"; + } +} + +void Quoter::reset() +{ + m_silent = false; + m_plainLines.clear(); + m_markedLines.clear(); + m_codeLocation = Location(); +} + +void Quoter::quoteFromFile(const QString &userFriendlyFilePath, const QString &plainCode, + const QString &markedCode) +{ + m_silent = false; + + /* + Split the source code into logical lines. Empty lines are + treated specially. Before: + + p->alpha(); + p->beta(); + + p->gamma(); + + + p->delta(); + + After: + + p->alpha(); + p->beta();\n + p->gamma();\n\n + p->delta(); + + Newlines are preserved because they affect codeLocation. + */ + m_codeLocation = Location(userFriendlyFilePath); + + m_plainLines = splitLines(plainCode); + m_markedLines = splitLines(markedCode); + if (m_markedLines.size() != m_plainLines.size()) { + m_codeLocation.warning( + QStringLiteral("Something is wrong with qdoc's handling of marked code")); + m_markedLines = m_plainLines; + } + + /* + Squeeze blanks (cat -s). + */ + for (auto &line : m_markedLines) + replaceMultipleNewlines(line); + m_codeLocation.start(); +} + +QString Quoter::quoteLine(const Location &docLocation, const QString &command, + const QString &pattern) +{ + if (m_plainLines.isEmpty()) { + failedAtEnd(docLocation, command); + return QString(); + } + + if (pattern.isEmpty()) { + docLocation.warning(QStringLiteral("Missing pattern after '\\%1'").arg(command)); + return QString(); + } + + if (match(docLocation, pattern, m_plainLines.first())) + return getLine(); + + if (!m_silent) { + docLocation.warning(QStringLiteral("Command '\\%1' failed").arg(command)); + m_codeLocation.warning(QStringLiteral("Pattern '%1' didn't match here").arg(pattern)); + m_silent = true; + } + return QString(); +} + +QString Quoter::quoteSnippet(const Location &docLocation, const QString &identifier) +{ + QString comment = commentForCode(); + QString delimiter = comment + QString(" [%1]").arg(identifier); + QString t; + int indent = 0; + + while (!m_plainLines.isEmpty()) { + if (match(docLocation, delimiter, m_plainLines.first())) { + QString startLine = getLine(); + while (indent < startLine.size() && startLine[indent] == QLatin1Char(' ')) + indent++; + break; + } + getLine(); + } + while (!m_plainLines.isEmpty()) { + QString line = m_plainLines.first(); + if (match(docLocation, delimiter, line)) { + QString lastLine = getLine(indent); + qsizetype dIndex = lastLine.indexOf(delimiter); + if (dIndex > 0) { + // The delimiter might be preceded on the line by other + // delimeters, so look for the first comment on the line. + QString leading = lastLine.left(dIndex); + dIndex = leading.indexOf(comment); + if (dIndex != -1) + leading = leading.left(dIndex); + if (leading.endsWith(QLatin1String("<@comment>"))) + leading.chop(10); + if (!leading.trimmed().isEmpty()) + t += leading; + } + return t; + } + + t += removeSpecialLines(line, comment, indent); + } + failedAtEnd(docLocation, QString("snippet (%1)").arg(delimiter)); + return t; +} + +QString Quoter::quoteTo(const Location &docLocation, const QString &command, const QString &pattern) +{ + QString t; + QString comment = commentForCode(); + + if (pattern.isEmpty()) { + while (!m_plainLines.isEmpty()) { + QString line = m_plainLines.first(); + t += removeSpecialLines(line, comment); + } + } else { + while (!m_plainLines.isEmpty()) { + if (match(docLocation, pattern, m_plainLines.first())) { + return t; + } + t += getLine(); + } + failedAtEnd(docLocation, command); + } + return t; +} + +QString Quoter::quoteUntil(const Location &docLocation, const QString &command, + const QString &pattern) +{ + QString t = quoteTo(docLocation, command, pattern); + t += getLine(); + return t; +} + +QString Quoter::getLine(int unindent) +{ + if (m_plainLines.isEmpty()) + return QString(); + + m_plainLines.removeFirst(); + + QString t = m_markedLines.takeFirst(); + int i = 0; + while (i < unindent && i < t.size() && t[i] == QLatin1Char(' ')) + i++; + + t = t.mid(i); + t += QLatin1Char('\n'); + m_codeLocation.advanceLines(t.count(QLatin1Char('\n'))); + return t; +} + +bool Quoter::match(const Location &docLocation, const QString &pattern0, const QString &line) +{ + QString str = line; + while (str.endsWith(QLatin1Char('\n'))) + str.truncate(str.size() - 1); + + QString pattern = pattern0; + if (pattern.startsWith(QLatin1Char('/')) && pattern.endsWith(QLatin1Char('/')) + && pattern.size() > 2) { + QRegularExpression rx(pattern.mid(1, pattern.size() - 2)); + if (!m_silent && !rx.isValid()) { + docLocation.warning( + QStringLiteral("Invalid regular expression '%1'").arg(rx.pattern())); + m_silent = true; + } + return str.indexOf(rx) != -1; + } + trimWhiteSpace(str); + trimWhiteSpace(pattern); + return str.indexOf(pattern) != -1; +} + +void Quoter::failedAtEnd(const Location &docLocation, const QString &command) +{ + if (!m_silent && !command.isEmpty()) { + if (m_codeLocation.filePath().isEmpty()) { + docLocation.warning(QStringLiteral("Unexpected '\\%1'").arg(command)); + } else { + docLocation.warning(QStringLiteral("Command '\\%1' failed at end of file '%2'") + .arg(command, m_codeLocation.filePath())); + } + m_silent = true; + } +} + +QString Quoter::commentForCode() const +{ + QFileInfo fi = QFileInfo(m_codeLocation.fileName()); + if (fi.fileName() == "CMakeLists.txt") + return "#!"; + return s_commentHash.value(fi.suffix(), "//!"); +} + +QString Quoter::removeSpecialLines(const QString &line, const QString &comment, int unindent) +{ + QString t; + + // Remove special macros to support Qt namespacing. + QString trimmed = line.trimmed(); + if (trimmed.startsWith("QT_BEGIN_NAMESPACE")) { + getLine(); + } else if (trimmed.startsWith("QT_END_NAMESPACE")) { + getLine(); + t += QLatin1Char('\n'); + } else if (!trimmed.startsWith(comment)) { + // Ordinary code + t += getLine(unindent); + } else { + // Comments + if (line.contains(QLatin1Char('\n'))) + t += QLatin1Char('\n'); + getLine(); + } + return t; +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/quoter.h b/src/qdoc/qdoc/src/qdoc/quoter.h new file mode 100644 index 000000000..087e9ffd2 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/quoter.h @@ -0,0 +1,45 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef QUOTER_H +#define QUOTER_H + +#include "location.h" + +#include <QtCore/qhash.h> +#include <QtCore/qstringlist.h> + +QT_BEGIN_NAMESPACE + +class Quoter +{ +public: + Quoter(); + + void reset(); + void quoteFromFile(const QString &userFriendlyFileName, const QString &plainCode, + const QString &markedCode); + QString quoteLine(const Location &docLocation, const QString &command, const QString &pattern); + QString quoteTo(const Location &docLocation, const QString &command, const QString &pattern); + QString quoteUntil(const Location &docLocation, const QString &command, const QString &pattern); + QString quoteSnippet(const Location &docLocation, const QString &identifier); + + static QStringList splitLines(const QString &line); + +private: + QString getLine(int unindent = 0); + void failedAtEnd(const Location &docLocation, const QString &command); + bool match(const Location &docLocation, const QString &pattern, const QString &line); + [[nodiscard]] QString commentForCode() const; + QString removeSpecialLines(const QString &line, const QString &comment, int unindent = 0); + + bool m_silent {}; + QStringList m_plainLines {}; + QStringList m_markedLines {}; + Location m_codeLocation {}; + static QHash<QString, QString> s_commentHash; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/relatedclass.cpp b/src/qdoc/qdoc/src/qdoc/relatedclass.cpp new file mode 100644 index 000000000..3eea8df92 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/relatedclass.cpp @@ -0,0 +1,46 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "relatedclass.h" + +#include "node.h" + +/*! + \struct RelatedClass + \brief A struct for indicating that a ClassNode is related in some way to another ClassNode. + + This struct has nothing to do with the \c {\\relates} command. This struct + is used for indicating that a ClassNode is a base class of another ClassNode, + is a derived class of another ClassNode, or is an ignored base class of + another ClassNode. This struct is only used in ClassNode. +*/ + +/*! \fn RelatedClass::RelatedClass() + The default constructor does nothing. It is only used for allocating empty + instances of RelatedClass in containers. + */ + +/*! \fn RelatedClass::RelatedClass(Access access, ClassNode *node) + This is the constructor used when the related class has been resolved. + In other words, when the ClassNode has been created so that \a node is + not \c nullptr. +*/ + +/*! \fn RelatedClass::RelatedClass(Access access, const QStringList &path, const QString &signature) + This is the constructor used when the related class has not bee resolved, + because it hasn't been created yet. In that case, we store the qualified + \a path name of the class and the \a signature of the class, which I think + is just the name of the class. + + \note We might be able to simplify the whole RelatedClass concept. Maybe we + can get rid of it completely. +*/ + +/*! \fn bool RelatedClass::isPrivate() const + Returns \c true if this RelatedClass is marked as Access::Private. +*/ +bool RelatedClass::isPrivate() const +{ + return (m_access == Access::Private); +} + diff --git a/src/qdoc/qdoc/src/qdoc/relatedclass.h b/src/qdoc/qdoc/src/qdoc/relatedclass.h new file mode 100644 index 000000000..9ca3b849a --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/relatedclass.h @@ -0,0 +1,34 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef RELATEDCLASS_H +#define RELATEDCLASS_H + +#include "access.h" + +#include <QtCore/qstring.h> +#include <QtCore/qstringlist.h> + +#include <utility> + +QT_BEGIN_NAMESPACE + +class ClassNode; + +struct RelatedClass +{ + RelatedClass() = default; + // constructor for resolved base class + RelatedClass(Access access, ClassNode *node) : m_access(access), m_node(node) {} + // constructor for unresolved base class + RelatedClass(Access access, QStringList path) : m_access(access), m_path(std::move(path)) { } + [[nodiscard]] bool isPrivate() const; + + Access m_access {}; + ClassNode *m_node { nullptr }; + QStringList m_path {}; +}; + +QT_END_NAMESPACE + +#endif // RELATEDCLASS_H diff --git a/src/qdoc/qdoc/src/qdoc/sections.cpp b/src/qdoc/qdoc/src/qdoc/sections.cpp new file mode 100644 index 000000000..bf066c08e --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/sections.cpp @@ -0,0 +1,992 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "sections.h" + +#include "aggregate.h" +#include "classnode.h" +#include "config.h" +#include "enumnode.h" +#include "functionnode.h" +#include "generator.h" +#include "utilities.h" +#include "namespacenode.h" +#include "qmlpropertynode.h" +#include "qmltypenode.h" +#include "sharedcommentnode.h" +#include "typedefnode.h" +#include "variablenode.h" + +#include <QtCore/qobjectdefs.h> + +QT_BEGIN_NAMESPACE + +QList<Section> Sections::s_stdSummarySections { + { "Namespaces", "namespace", "namespaces", "", Section::Summary }, + { "Classes", "class", "classes", "", Section::Summary }, + { "Types", "type", "types", "", Section::Summary }, + { "Variables", "variable", "variables", "", Section::Summary }, + { "Static Variables", "static variable", "static variables", "", Section::Summary }, + { "Functions", "function", "functions", "", Section::Summary }, + { "Macros", "macro", "macros", "", Section::Summary }, +}; + +QList<Section> Sections::s_stdDetailsSections { + { "Namespaces", "namespace", "namespaces", "nmspace", Section::Details }, + { "Classes", "class", "classes", "classes", Section::Details }, + { "Type Documentation", "type", "types", "types", Section::Details }, + { "Variable Documentation", "variable", "variables", "vars", Section::Details }, + { "Static Variables", "static variable", "static variables", QString(), Section::Details }, + { "Function Documentation", "function", "functions", "func", Section::Details }, + { "Macro Documentation", "macro", "macros", "macros", Section::Details }, +}; + +QList<Section> Sections::s_stdCppClassSummarySections { + { "Public Types", "public type", "public types", "", Section::Summary }, + { "Properties", "property", "properties", "", Section::Summary }, + { "Public Functions", "public function", "public functions", "", Section::Summary }, + { "Public Slots", "public slot", "public slots", "", Section::Summary }, + { "Signals", "signal", "signals", "", Section::Summary }, + { "Public Variables", "public variable", "public variables", "", Section::Summary }, + { "Static Public Members", "static public member", "static public members", "", Section::Summary }, + { "Protected Types", "protected type", "protected types", "", Section::Summary }, + { "Protected Functions", "protected function", "protected functions", "", Section::Summary }, + { "Protected Slots", "protected slot", "protected slots", "", Section::Summary }, + { "Protected Variables", "protected type", "protected variables", "", Section::Summary }, + { "Static Protected Members", "static protected member", "static protected members", "", Section::Summary }, + { "Private Types", "private type", "private types", "", Section::Summary }, + { "Private Functions", "private function", "private functions", "", Section::Summary }, + { "Private Slots", "private slot", "private slots", "", Section::Summary }, + { "Static Private Members", "static private member", "static private members", "", Section::Summary }, + { "Related Non-Members", "related non-member", "related non-members", "", Section::Summary }, + { "Macros", "macro", "macros", "", Section::Summary }, +}; + +QList<Section> Sections::s_stdCppClassDetailsSections { + { "Member Type Documentation", "member", "members", "types", Section::Details }, + { "Property Documentation", "member", "members", "prop", Section::Details }, + { "Member Function Documentation", "member", "members", "func", Section::Details }, + { "Member Variable Documentation", "member", "members", "vars", Section::Details }, + { "Related Non-Members", "member", "members", "relnonmem", Section::Details }, + { "Macro Documentation", "member", "members", "macros", Section::Details }, +}; + +QList<Section> Sections::s_stdQmlTypeSummarySections { + { "Properties", "property", "properties", "", Section::Summary }, + { "Attached Properties", "attached property", "attached properties", "", Section::Summary }, + { "Signals", "signal", "signals", "", Section::Summary }, + { "Signal Handlers", "signal handler", "signal handlers", "", Section::Summary }, + { "Attached Signals", "attached signal", "attached signals", "", Section::Summary }, + { "Methods", "method", "methods", "", Section::Summary }, + { "Attached Methods", "attached method", "attached methods", "", Section::Summary }, +}; + +QList<Section> Sections::s_stdQmlTypeDetailsSections { + { "Property Documentation", "member", "members", "qmlprop", Section::Details }, + { "Attached Property Documentation", "member", "members", "qmlattprop", Section::Details }, + { "Signal Documentation", "signal", "signals", "qmlsig", Section::Details }, + { "Signal Handler Documentation", "signal handler", "signal handlers", "qmlsighan", Section::Details }, + { "Attached Signal Documentation", "signal", "signals", "qmlattsig", Section::Details }, + { "Method Documentation", "member", "members", "qmlmeth", Section::Details }, + { "Attached Method Documentation", "member", "members", "qmlattmeth", Section::Details }, +}; + +QList<Section> Sections::s_sinceSections { + { "New Namespaces", "", "", "", Section::Details }, + { "New Classes", "", "", "", Section::Details }, + { "New Member Functions", "", "", "", Section::Details }, + { "New Functions in Namespaces", "", "", "", Section::Details }, + { "New Global Functions", "", "", "", Section::Details }, + { "New Macros", "", "", "", Section::Details }, + { "New Enum Types", "", "", "", Section::Details }, + { "New Enum Values", "", "", "", Section::Details }, + { "New Type Aliases", "", "", "", Section::Details }, + { "New Properties", "", "", "", Section::Details }, + { "New Variables", "", "", "", Section::Details }, + { "New QML Types", "", "", "", Section::Details }, + { "New QML Properties", "", "", "", Section::Details }, + { "New QML Signals", "", "", "", Section::Details }, + { "New QML Signal Handlers", "", "", "", Section::Details }, + { "New QML Methods", "", "", "", Section::Details }, +}; + +QList<Section> Sections::s_allMembers{ { "", "member", "members", "", Section::AllMembers } }; + +/*! + \class Section + \brief A class for containing the elements of one documentation section + */ + +/*! + The destructor must delete the members of collections + when the members are allocated on the heap. + */ +Section::~Section() +{ + clear(); +} + +/*! + A Section is now an element in a static vector, so we + don't have to repeatedly construct and destroy them. But + we do need to clear them before each call to build the + sections for a C++ or QML entity. + */ +void Section::clear() +{ + m_reimplementedMemberMap.clear(); + m_members.clear(); + m_obsoleteMembers.clear(); + m_reimplementedMembers.clear(); + m_inheritedMembers.clear(); + m_classNodesList.clear(); + m_aggregate = nullptr; +} + +/*! + Construct a name for the \a node that can be used for sorting + a set of nodes into equivalence classes. + */ +QString sortName(const Node *node) +{ + QString nodeName{node->name()}; + + int numDigits = 0; + for (qsizetype i = nodeName.size() - 1; i > 0; --i) { + if (nodeName.at(i).digitValue() == -1) + break; + ++numDigits; + } + + // we want 'qint8' to appear before 'qint16' + if (numDigits > 0) { + for (int i = 0; i < 4 - numDigits; ++i) + nodeName.insert(nodeName.size() - numDigits - 1, QLatin1Char('0')); + } + + if (node->isClassNode()) + return QLatin1Char('A') + nodeName; + + if (node->isFunction(Node::CPP)) { + const auto *fn = static_cast<const FunctionNode *>(node); + + QString sortNo; + if (fn->isCtor()) + sortNo = QLatin1String("C"); + else if (fn->isCCtor()) + sortNo = QLatin1String("D"); + else if (fn->isMCtor()) + sortNo = QLatin1String("E"); + else if (fn->isDtor()) + sortNo = QLatin1String("F"); + else if (nodeName.startsWith(QLatin1String("operator")) && nodeName.size() > 8 + && !nodeName[8].isLetterOrNumber()) + sortNo = QLatin1String("H"); + else + sortNo = QLatin1String("G"); + + return sortNo + nodeName + QLatin1Char(' ') + QString::number(fn->overloadNumber(), 36); + } + + if (node->isFunction(Node::QML)) + return QLatin1Char('E') + nodeName + QLatin1Char(' ') + + QString::number(static_cast<const FunctionNode*>(node)->overloadNumber(), 36); + + if (node->isProperty() || node->isVariable()) + return QLatin1Char('G') + nodeName; + + return QLatin1Char('B') + nodeName; +} + +/*! + Inserts the \a node into this section if it is appropriate + for this section. + */ +void Section::insert(Node *node) +{ + bool irrelevant = false; + bool inherited = false; + if (!node->isRelatedNonmember()) { + Aggregate *p = node->parent(); + if (!p->isNamespace() && p != m_aggregate) { + if (!p->isQmlType() || !p->isAbstract()) + inherited = true; + } + } + + if (node->isPrivate() || node->isInternal()) { + irrelevant = true; + } else if (node->isFunction()) { + auto *func = static_cast<FunctionNode *>(node); + irrelevant = (inherited && (func->isSomeCtor() || func->isDtor())); + } else if (node->isClassNode() || node->isEnumType() || node->isTypedef() + || node->isVariable()) { + irrelevant = (inherited && m_style != AllMembers); + if (!irrelevant && m_style == Details && node->isTypedef()) { + const auto *tdn = static_cast<const TypedefNode *>(node); + if (tdn->associatedEnum()) + irrelevant = true; + } + } + + if (!irrelevant) { + QString key = sortName(node); + if (node->isDeprecated()) { + m_obsoleteMembers.push_back(node); + } else { + if (!inherited || m_style == AllMembers) + m_members.push_back(node); + + if (inherited && (node->parent()->isClassNode() || node->parent()->isNamespace())) { + if (m_inheritedMembers.isEmpty() + || m_inheritedMembers.last().first != node->parent()) { + std::pair<Aggregate *, int> p(node->parent(), 0); + m_inheritedMembers.append(p); + } + m_inheritedMembers.last().second++; + } + } + } +} + +/*! + Returns \c true if the \a node is a reimplemented member + function of the current class. If true, the \a node is + inserted into the reimplemented member map. True + is returned only if \a node is inserted into the map. + That is, false is returned if the \a node is already in + the map. + */ +bool Section::insertReimplementedMember(Node *node) +{ + if (!node->isPrivate() && !node->isRelatedNonmember()) { + const auto *fn = static_cast<const FunctionNode *>(node); + if (!fn->overridesThis().isEmpty()) { + if (fn->parent() == m_aggregate) { + QString key = sortName(fn); + if (!m_reimplementedMemberMap.contains(key)) { + m_reimplementedMemberMap.insert(key, node); + return true; + } + } + } + } + return false; +} + +/*! + If this section is not empty, convert its maps to sequential + structures for better traversal during doc generation. + */ +void Section::reduce() +{ + // TODO:TEMPORARY:INTERMEDITATE: Section uses a series of maps + // to internally manage the categorization of the various members + // of an aggregate. It further uses a secondary "flattened" + // (usually vector) version that is later used by consumers of a + // Section content. + // + // One of the uses of those maps is that of ordering, by using + // keys generated with `sortName`. + // Nonetheless, this is the only usage that comes from the keys, + // as they are neither necessary nor used outside of the internal + // code for Section. + // + // Hence, the codebase is moving towards removing the maps in + // favor of building a flattened, consumer ready, version of the + // categorization directly, cutting the intermediate conversion + // step. + // + // To do so while keeping as much of the old behavior as possible, + // we provide a sorting for the flattened version that is based on + // `sortName`, as the previous ordering was. + // + // This acts as a relatively heavy pessimization, as `sortName`, + // used as a comparator, can be called multiple times for each + // Node, while before it would have been called almost-once. + // + // Instead of fixing this issue, by for example caching the + // sortName of each Node instance, we temporarily keep the + // pessimization while the various maps are removed. + // + // When all the maps are removed, we can remove `sortName`, which + // produces strings to use as key requiring a few allocations and + // expensive operations, with an actual comparator function, which + // should be more lightweight and more than offset the + // multiple-calls. + static auto node_less_than = [](const Node* left, const Node* right) { + return sortName(left) < sortName(right); + }; + + std::stable_sort(m_members.begin(), m_members.end(), node_less_than); + std::stable_sort(m_obsoleteMembers.begin(), m_obsoleteMembers.end(), node_less_than); + + m_reimplementedMembers = m_reimplementedMemberMap.values().toVector(); + + for (auto &cn : m_classNodesList) { + std::stable_sort(cn.second.begin(), cn.second.end(), node_less_than); + } +} + +/*! + \class Sections + \brief A class for creating vectors of collections for documentation + + Each element in a vector is an instance of Section, which + contains all the elements that will be documented in one + section of a reference documentation page. + */ + +/*! + This constructor builds the vectors of sections based on the + type of the \a aggregate node. + */ +Sections::Sections(Aggregate *aggregate) : m_aggregate(aggregate) +{ + initAggregate(s_allMembers, m_aggregate); + switch (m_aggregate->nodeType()) { + case Node::Class: + case Node::Struct: + case Node::Union: + initAggregate(s_stdCppClassSummarySections, m_aggregate); + initAggregate(s_stdCppClassDetailsSections, m_aggregate); + buildStdCppClassRefPageSections(); + break; + case Node::QmlType: + case Node::QmlValueType: + initAggregate(s_stdQmlTypeSummarySections, m_aggregate); + initAggregate(s_stdQmlTypeDetailsSections, m_aggregate); + buildStdQmlTypeRefPageSections(); + break; + case Node::Namespace: + case Node::HeaderFile: + case Node::Proxy: + default: + initAggregate(s_stdSummarySections, m_aggregate); + initAggregate(s_stdDetailsSections, m_aggregate); + buildStdRefPageSections(); + break; + } +} + +/*! + This constructor builds a vector of sections from the \e since + node map, \a nsmap + */ +Sections::Sections(const NodeMultiMap &nsmap) : m_aggregate(nullptr) +{ + if (nsmap.isEmpty()) + return; + SectionVector §ions = sinceSections(); + for (auto it = nsmap.constBegin(); it != nsmap.constEnd(); ++it) { + Node *node = it.value(); + switch (node->nodeType()) { + case Node::QmlType: + sections[SinceQmlTypes].appendMember(node); + break; + case Node::Namespace: + sections[SinceNamespaces].appendMember(node); + break; + case Node::Class: + case Node::Struct: + case Node::Union: + sections[SinceClasses].appendMember(node); + break; + case Node::Enum: { + // The map can contain an enum node with \since, or an enum node + // with \value containing a since-clause. In the latter case, + // key() is an empty string. + if (!it.key().isEmpty()) + sections[SinceEnumTypes].appendMember(node); + else + sections[SinceEnumValues].appendMember(node); + break; + } + case Node::Typedef: + case Node::TypeAlias: + sections[SinceTypeAliases].appendMember(node); + break; + case Node::Function: { + const auto *fn = static_cast<const FunctionNode *>(node); + switch (fn->metaness()) { + case FunctionNode::QmlSignal: + sections[SinceQmlSignals].appendMember(node); + break; + case FunctionNode::QmlSignalHandler: + sections[SinceQmlSignalHandlers].appendMember(node); + break; + case FunctionNode::QmlMethod: + sections[SinceQmlMethods].appendMember(node); + break; + default: + if (fn->isMacro()) + sections[SinceMacros].appendMember(node); + else { + Node *p = fn->parent(); + if (p) { + if (p->isClassNode()) + sections[SinceMemberFunctions].appendMember(node); + else if (p->isNamespace()) { + if (p->name().isEmpty()) + sections[SinceGlobalFunctions].appendMember(node); + else + sections[SinceNamespaceFunctions].appendMember(node); + } else + sections[SinceGlobalFunctions].appendMember(node); + } else + sections[SinceGlobalFunctions].appendMember(node); + } + break; + } + break; + } + case Node::Property: + sections[SinceProperties].appendMember(node); + break; + case Node::Variable: + sections[SinceVariables].appendMember(node); + break; + case Node::QmlProperty: + sections[SinceQmlProperties].appendMember(node); + break; + default: + break; + } + } +} + +/*! + The behavior of the destructor depends on the type of the + Aggregate node that was passed to the constructor. If the + constructor was passed a multimap, the destruction is a + bit different because there was no Aggregate node. + */ +Sections::~Sections() +{ + if (m_aggregate) { + switch (m_aggregate->nodeType()) { + case Node::Class: + case Node::Struct: + case Node::Union: + clear(stdCppClassSummarySections()); + clear(stdCppClassDetailsSections()); + allMembersSection().clear(); + break; + case Node::QmlType: + case Node::QmlValueType: + clear(stdQmlTypeSummarySections()); + clear(stdQmlTypeDetailsSections()); + allMembersSection().clear(); + break; + default: + clear(stdSummarySections()); + clear(stdDetailsSections()); + allMembersSection().clear(); + break; + } + m_aggregate = nullptr; + } else { + clear(sinceSections()); + } +} + +/*! + Initialize the Aggregate in each Section of vector \a v with \a aggregate. + */ +void Sections::initAggregate(SectionVector &v, Aggregate *aggregate) +{ + for (Section §ion : v) + section.setAggregate(aggregate); +} + +/*! + Reset each Section in vector \a v to its initialized state. + */ +void Sections::clear(QList<Section> &v) +{ + for (Section §ion : v) + section.clear(); +} + +/*! + Linearize the maps in each Section in \a v. + */ +void Sections::reduce(QList<Section> &v) +{ + for (Section §ion : v) + section.reduce(); +} + +/*! + This is a private helper function for buildStdRefPageSections(). + */ +void Sections::stdRefPageSwitch(SectionVector &v, Node *n, Node *t) +{ + // t is the reference node to be tested, n is the node to be distributed. + // t differs from n only for shared comment nodes. + if (!t) + t = n; + + switch (t->nodeType()) { + case Node::Namespace: + v[StdNamespaces].insert(n); + return; + case Node::Class: + case Node::Struct: + case Node::Union: + v[StdClasses].insert(n); + return; + case Node::Enum: + case Node::Typedef: + case Node::TypeAlias: + v[StdTypes].insert(n); + return; + case Node::Function: { + auto *func = static_cast<FunctionNode *>(t); + if (func->isMacro()) + v[StdMacros].insert(n); + else + v[StdFunctions].insert(n); + } + return; + case Node::Variable: { + const auto *var = static_cast<const VariableNode *>(t); + if (!var->doc().isEmpty()) { + if (var->isStatic()) + v[StdStaticVariables].insert(n); + else + v[StdVariables].insert(n); + } + } + return; + case Node::SharedComment: { + auto *scn = static_cast<SharedCommentNode *>(t); + if (!scn->doc().isEmpty() && scn->collective().size()) + stdRefPageSwitch( + v, scn, + scn->collective().first()); // TODO: warn about mixed node types in collective? + } + return; + default: + return; + } +} + +/*! + Build the section vectors for a standard reference page, + when the aggregate node is not a C++ class or a QML type. + + If this is for a namespace page then if the namespace node + itself does not have documentation, only its children that + have documentation should be documented. In other words, + there are cases where a namespace is declared but does not + have documentation, but some of the elements declared in + that namespace do have documentation. + + This special processing of namespaces that do not have a + documentation comment is meant to allow documenting its + members that do have documentation while avoiding posting + error messages for its members that are not documented. + */ +void Sections::buildStdRefPageSections() +{ + const NamespaceNode *ns = nullptr; + bool documentAll = true; // document all the children + if (m_aggregate->isNamespace()) { + ns = static_cast<const NamespaceNode *>(m_aggregate); + if (!ns->hasDoc()) + documentAll = false; // only document children that have documentation + } + for (auto it = m_aggregate->constBegin(); it != m_aggregate->constEnd(); ++it) { + Node *n = *it; + if (documentAll || n->hasDoc()) { + stdRefPageSwitch(stdSummarySections(), n); + stdRefPageSwitch(stdDetailsSections(), n); + } + } + if (!m_aggregate->relatedByProxy().isEmpty()) { + const QList<Node *> &relatedBy = m_aggregate->relatedByProxy(); + for (const auto &node : relatedBy) + stdRefPageSwitch(stdSummarySections(), node); + } + /* + If we are building the sections for the reference page + for a namespace node, include all the namespace node's + included children in the sections. + */ + if (ns && !ns->includedChildren().isEmpty()) { + const QList<Node *> &children = ns->includedChildren(); + for (const auto &child : children) { + if (documentAll || child->hasDoc()) + stdRefPageSwitch(stdSummarySections(), child); + } + } + reduce(stdSummarySections()); + reduce(stdDetailsSections()); + allMembersSection().reduce(); +} + +/*! + Inserts the node \a n in one of the entries in the vector \a v + depending on the node's type, access attribute, and a few other + attributes if the node is a signal, slot, or function. + */ +void Sections::distributeNodeInSummaryVector(SectionVector &sv, Node *n) +{ + if (n->isSharedCommentNode()) + return; + if (n->isFunction()) { + auto *fn = static_cast<FunctionNode *>(n); + if (fn->isRelatedNonmember()) { + if (fn->isMacro()) + sv[Macros].insert(n); + else + sv[RelatedNonmembers].insert(n); + return; + } + if (fn->isIgnored()) + return; + if (fn->isSlot()) { + if (fn->isPublic()) + sv[PublicSlots].insert(fn); + else if (fn->isPrivate()) + sv[PrivateSlots].insert(fn); + else + sv[ProtectedSlots].insert(fn); + } else if (fn->isSignal()) { + if (fn->isPublic()) + sv[Signals].insert(fn); + } else if (fn->isPublic()) { + if (fn->isStatic()) + sv[StaticPublicMembers].insert(fn); + else if (!sv[PublicFunctions].insertReimplementedMember(fn)) + sv[PublicFunctions].insert(fn); + } else if (fn->isPrivate()) { + if (fn->isStatic()) + sv[StaticPrivateMembers].insert(fn); + else if (!sv[PrivateFunctions].insertReimplementedMember(fn)) + sv[PrivateFunctions].insert(fn); + } else { // protected + if (fn->isStatic()) + sv[StaticProtectedMembers].insert(fn); + else if (!sv[ProtectedFunctions].insertReimplementedMember(fn)) + sv[ProtectedFunctions].insert(fn); + } + return; + } + if (n->isRelatedNonmember()) { + sv[RelatedNonmembers].insert(n); + return; + } + if (n->isVariable()) { + if (n->isStatic()) { + if (n->isPublic()) + sv[StaticPublicMembers].insert(n); + else if (n->isPrivate()) + sv[StaticPrivateMembers].insert(n); + else + sv[StaticProtectedMembers].insert(n); + } else { + if (n->isPublic()) + sv[PublicVariables].insert(n); + else if (!n->isPrivate()) + sv[ProtectedVariables].insert(n); + } + return; + } + /* + Getting this far means the node is either a property + or some kind of type, like an enum or a typedef. + */ + if (n->isTypedef() && (n->name() == QLatin1String("QtGadgetHelper"))) + return; + if (n->isProperty()) + sv[Properties].insert(n); + else if (n->isPublic()) + sv[PublicTypes].insert(n); + else if (n->isPrivate()) + sv[PrivateTypes].insert(n); + else + sv[ProtectedTypes].insert(n); +} + +/*! + Inserts the node \a n in one of the entries in the vector \a v + depending on the node's type, access attribute, and a few other + attributes if the node is a signal, slot, or function. + */ +void Sections::distributeNodeInDetailsVector(SectionVector &dv, Node *n) +{ + if (n->isSharingComment()) + return; + + // t is the reference node to be tested - typically it's this node (n), but for + // shared comment nodes we need to distribute based on the nodes in its collective. + Node *t = n; + + if (n->isSharedCommentNode() && n->hasDoc()) { + auto *scn = static_cast<SharedCommentNode *>(n); + if (scn->collective().size()) + t = scn->collective().first(); // TODO: warn about mixed node types in collective? + } + + if (t->isFunction()) { + auto *fn = static_cast<FunctionNode *>(t); + if (fn->isRelatedNonmember()) { + if (fn->isMacro()) + dv[DetailsMacros].insert(n); + else + dv[DetailsRelatedNonmembers].insert(n); + return; + } + if (fn->isIgnored()) + return; + if (!fn->hasAssociatedProperties() || !fn->doc().isEmpty()) + dv[DetailsMemberFunctions].insert(n); + return; + } + if (t->isRelatedNonmember()) { + dv[DetailsRelatedNonmembers].insert(n); + return; + } + if (t->isEnumType() || t->isTypedef()) { + if (t->name() != QLatin1String("QtGadgetHelper")) + dv[DetailsMemberTypes].insert(n); + return; + } + if (t->isProperty()) + dv[DetailsProperties].insert(n); + else if (t->isVariable() && !t->doc().isEmpty()) + dv[DetailsMemberVariables].insert(n); +} + +void Sections::distributeQmlNodeInDetailsVector(SectionVector &dv, Node *n) +{ + if (n->isSharingComment()) + return; + + // t is the reference node to be tested - typically it's this node (n), but for + // shared comment nodes we need to distribute based on the nodes in its collective. + Node *t = n; + + if (n->isSharedCommentNode() && n->hasDoc()) { + if (n->isPropertyGroup()) { + dv[QmlProperties].insert(n); + return; + } + auto *scn = static_cast<SharedCommentNode *>(n); + if (scn->collective().size()) + t = scn->collective().first(); // TODO: warn about mixed node types in collective? + } + + if (t->isQmlProperty()) { + auto *pn = static_cast<QmlPropertyNode *>(t); + if (pn->isAttached()) + dv[QmlAttachedProperties].insert(n); + else + dv[QmlProperties].insert(n); + } else if (t->isFunction()) { + auto *fn = static_cast<FunctionNode *>(t); + if (fn->isQmlSignal()) { + if (fn->isAttached()) + dv[QmlAttachedSignals].insert(n); + else + dv[QmlSignals].insert(n); + } else if (fn->isQmlSignalHandler()) { + dv[QmlSignalHandlers].insert(n); + } else if (fn->isQmlMethod()) { + if (fn->isAttached()) + dv[QmlAttachedMethods].insert(n); + else + dv[QmlMethods].insert(n); + } + } +} + +/*! + Distributes a node \a n into the correct place in the summary section vector \a sv. + Nodes that are sharing a comment are handled recursively - for recursion, the \a + sharing parameter is set to \c true. + */ +void Sections::distributeQmlNodeInSummaryVector(SectionVector &sv, Node *n, bool sharing) +{ + if (n->isSharingComment() && !sharing) + return; + if (n->isQmlProperty()) { + auto *pn = static_cast<QmlPropertyNode *>(n); + if (pn->isAttached()) + sv[QmlAttachedProperties].insert(pn); + else + sv[QmlProperties].insert(pn); + } else if (n->isFunction()) { + auto *fn = static_cast<FunctionNode *>(n); + if (fn->isQmlSignal()) { + if (fn->isAttached()) + sv[QmlAttachedSignals].insert(fn); + else + sv[QmlSignals].insert(fn); + } else if (fn->isQmlSignalHandler()) { + sv[QmlSignalHandlers].insert(fn); + } else if (fn->isQmlMethod()) { + if (fn->isAttached()) + sv[QmlAttachedMethods].insert(fn); + else + sv[QmlMethods].insert(fn); + } + } else if (n->isSharedCommentNode()) { + auto *scn = static_cast<SharedCommentNode *>(n); + if (scn->isPropertyGroup()) { + sv[QmlProperties].insert(scn); + } else { + for (const auto &child : scn->collective()) + distributeQmlNodeInSummaryVector(sv, child, true); + } + } +} + +static void pushBaseClasses(QStack<ClassNode *> &stack, ClassNode *cn) +{ + const QList<RelatedClass> baseClasses = cn->baseClasses(); + for (const auto &cls : baseClasses) { + if (cls.m_node) + stack.prepend(cls.m_node); + } +} + +/*! + Build the section vectors for a standard reference page, + when the aggregate node is a C++. + */ +void Sections::buildStdCppClassRefPageSections() +{ + SectionVector &summarySections = stdCppClassSummarySections(); + SectionVector &detailsSections = stdCppClassDetailsSections(); + Section &allMembers = allMembersSection(); + + for (auto it = m_aggregate->constBegin(); it != m_aggregate->constEnd(); ++it) { + Node *n = *it; + if (!n->isPrivate() && !n->isProperty() && !n->isRelatedNonmember() + && !n->isSharedCommentNode()) + allMembers.insert(n); + + distributeNodeInSummaryVector(summarySections, n); + distributeNodeInDetailsVector(detailsSections, n); + } + if (!m_aggregate->relatedByProxy().isEmpty()) { + const QList<Node *> relatedBy = m_aggregate->relatedByProxy(); + for (const auto &node : relatedBy) + distributeNodeInSummaryVector(summarySections, node); + } + + QStack<ClassNode *> stack; + auto *cn = static_cast<ClassNode *>(m_aggregate); + pushBaseClasses(stack, cn); + while (!stack.isEmpty()) { + ClassNode *cn = stack.pop(); + for (auto it = cn->constBegin(); it != cn->constEnd(); ++it) { + Node *n = *it; + if (!n->isPrivate() && !n->isProperty() && !n->isRelatedNonmember() + && !n->isSharedCommentNode()) + allMembers.insert(n); + } + pushBaseClasses(stack, cn); + } + reduce(summarySections); + reduce(detailsSections); + allMembers.reduce(); +} + +/*! + Build the section vectors for a standard reference page, + when the aggregate node is a QML type. + */ +void Sections::buildStdQmlTypeRefPageSections() +{ + ClassNodes *classNodes = nullptr; + SectionVector &summarySections = stdQmlTypeSummarySections(); + SectionVector &detailsSections = stdQmlTypeDetailsSections(); + Section &allMembers = allMembersSection(); + + const Aggregate *qtn = m_aggregate; + while (qtn) { + if (!qtn->isAbstract() || !classNodes) + classNodes = &allMembers.classNodesList().emplace_back(static_cast<const QmlTypeNode*>(qtn), NodeVector{}); + for (const auto n : qtn->childNodes()) { + if (n->isInternal()) + continue; + + // Skip overridden property/function documentation from abstract base type + if (qtn != m_aggregate && qtn->isAbstract()) { + NodeList candidates; + m_aggregate->findChildren(n->name(), candidates); + if (std::any_of(candidates.cbegin(), candidates.cend(), [&n](const Node *c) { + if (c->nodeType() == n->nodeType()) { + if (!n->isFunction() || + compare(static_cast<const FunctionNode *>(n), + static_cast<const FunctionNode *>(c)) == 0) + return true; + } + return false; + })) { + continue; + } + } + + if (!n->isSharedCommentNode() || n->isPropertyGroup()) { + allMembers.insert(n); + classNodes->second.push_back(n); + } + + + if (qtn == m_aggregate || qtn->isAbstract()) { + distributeQmlNodeInSummaryVector(summarySections, n); + distributeQmlNodeInDetailsVector(detailsSections, n); + } + } + if (qtn->qmlBaseNode() == qtn) { + qCDebug(lcQdoc, "error: circular type definition: '%s' inherits itself", + qPrintable(qtn->name())); + break; + } + qtn = static_cast<QmlTypeNode *>(qtn->qmlBaseNode()); + } + + reduce(summarySections); + reduce(detailsSections); + allMembers.reduce(); +} + +/*! + Returns true if any sections in this object contain obsolete + members. If it returns false, then \a summary_spv and \a details_spv + have not been modified. Otherwise, both vectors will contain pointers + to the sections that contain obsolete members. + */ +bool Sections::hasObsoleteMembers(SectionPtrVector *summary_spv, + SectionPtrVector *details_spv) const +{ + const SectionVector *sections = nullptr; + if (m_aggregate->isClassNode()) + sections = &stdCppClassSummarySections(); + else if (m_aggregate->isQmlType()) + sections = &stdQmlTypeSummarySections(); + else + sections = &stdSummarySections(); + for (const auto §ion : *sections) { + if (!section.obsoleteMembers().isEmpty()) + summary_spv->append(§ion); + } + if (m_aggregate->isClassNode()) + sections = &stdCppClassDetailsSections(); + else if (m_aggregate->isQmlType()) + sections = &stdQmlTypeDetailsSections(); + else + sections = &stdDetailsSections(); + for (const auto &it : *sections) { + if (!it.obsoleteMembers().isEmpty()) + details_spv->append(&it); + } + return !summary_spv->isEmpty(); +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/sections.h b/src/qdoc/qdoc/src/qdoc/sections.h new file mode 100644 index 000000000..70c73a91c --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/sections.h @@ -0,0 +1,206 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef SECTIONS_H +#define SECTIONS_H + +#include "node.h" + +QT_BEGIN_NAMESPACE + +class Aggregate; + +typedef std::pair<const QmlTypeNode *, NodeVector> ClassNodes; +typedef QList<ClassNodes> ClassNodesList; + +class Section +{ +public: + enum Style { Summary, Details, AllMembers, Accessors }; + +public: + Section( + QString title, QString singular, QString plural, + QString divclass, Style style + ) : m_title{title}, m_singular{singular}, m_plural{plural}, + m_divClass{divclass}, m_style{style} + {} + + ~Section(); + + void insert(Node *node); + bool insertReimplementedMember(Node *node); + + void appendMember(Node *node) { m_members.append(node); } + + void clear(); + void reduce(); + [[nodiscard]] bool isEmpty() const + { + return (m_members.isEmpty() && m_inheritedMembers.isEmpty() + && m_reimplementedMemberMap.isEmpty() && m_classNodesList.isEmpty()); + } + + [[nodiscard]] Style style() const { return m_style; } + [[nodiscard]] const QString &title() const { return m_title; } + [[nodiscard]] const QString &divClass() const { return m_divClass; } + [[nodiscard]] const QString &singular() const { return m_singular; } + [[nodiscard]] const QString &plural() const { return m_plural; } + [[nodiscard]] const NodeVector &members() const { return m_members; } + [[nodiscard]] const NodeVector &reimplementedMembers() const { return m_reimplementedMembers; } + [[nodiscard]] const QList<std::pair<Aggregate *, int>> &inheritedMembers() const + { + return m_inheritedMembers; + } + ClassNodesList &classNodesList() { return m_classNodesList; } + [[nodiscard]] const NodeVector &obsoleteMembers() const { return m_obsoleteMembers; } + void appendMembers(const NodeVector &nv) { m_members.append(nv); } + [[nodiscard]] const Aggregate *aggregate() const { return m_aggregate; } + void setAggregate(Aggregate *t) { m_aggregate = t; } + +private: + QString m_title {}; + QString m_singular {}; + QString m_plural {}; + QString m_divClass {}; + Style m_style {}; + + Aggregate *m_aggregate { nullptr }; + NodeVector m_members {}; + NodeVector m_obsoleteMembers {}; + NodeVector m_reimplementedMembers {}; + QList<std::pair<Aggregate *, int>> m_inheritedMembers {}; + ClassNodesList m_classNodesList {}; + + QMultiMap<QString, Node *> m_reimplementedMemberMap {}; +}; + +typedef QList<Section> SectionVector; +typedef QList<const Section *> SectionPtrVector; + +class Sections +{ +public: + enum VectorIndex { + PublicTypes = 0, + DetailsMemberTypes = 0, + SinceNamespaces = 0, + StdNamespaces = 0, + QmlProperties = 0, + Properties = 1, + DetailsProperties = 1, + SinceClasses = 1, + StdClasses = 1, + QmlAttachedProperties = 1, + PublicFunctions = 2, + DetailsMemberFunctions = 2, + SinceMemberFunctions = 2, + StdTypes = 2, + QmlSignals = 2, + PublicSlots = 3, + DetailsMemberVariables = 3, + SinceNamespaceFunctions = 3, + StdVariables = 3, + QmlSignalHandlers = 3, + Signals = 4, + SinceGlobalFunctions = 4, + DetailsRelatedNonmembers = 4, + StdStaticVariables = 4, + QmlAttachedSignals = 4, + PublicVariables = 5, + SinceMacros = 5, + DetailsMacros = 5, + StdFunctions = 5, + QmlMethods = 5, + StaticPublicMembers = 6, + SinceEnumTypes = 6, + StdMacros = 6, + QmlAttachedMethods = 6, + SinceEnumValues = 7, + ProtectedTypes = 7, + SinceTypeAliases = 8, + ProtectedFunctions = 8, + SinceProperties = 9, + ProtectedSlots = 9, + SinceVariables = 10, + ProtectedVariables = 10, + SinceQmlTypes = 11, + StaticProtectedMembers = 11, + SinceQmlProperties = 12, + PrivateTypes = 12, + SinceQmlSignals = 13, + PrivateFunctions = 13, + SinceQmlSignalHandlers = 14, + PrivateSlots = 14, + SinceQmlMethods = 15, + StaticPrivateMembers = 15, + RelatedNonmembers = 16, + Macros = 17 + }; + + explicit Sections(Aggregate *aggregate); + explicit Sections(const NodeMultiMap &nsmap); + ~Sections(); + + void clear(SectionVector &v); + void reduce(SectionVector &v); + void buildStdRefPageSections(); + void buildStdCppClassRefPageSections(); + void buildStdQmlTypeRefPageSections(); + + bool hasObsoleteMembers(SectionPtrVector *summary_spv, SectionPtrVector *details_spv) const; + + static Section &allMembersSection() { return s_allMembers[0]; } + SectionVector &sinceSections() { return s_sinceSections; } + SectionVector &stdSummarySections() { return s_stdSummarySections; } + SectionVector &stdDetailsSections() { return s_stdDetailsSections; } + SectionVector &stdCppClassSummarySections() { return s_stdCppClassSummarySections; } + SectionVector &stdCppClassDetailsSections() { return s_stdCppClassDetailsSections; } + SectionVector &stdQmlTypeSummarySections() { return s_stdQmlTypeSummarySections; } + SectionVector &stdQmlTypeDetailsSections() { return s_stdQmlTypeDetailsSections; } + + [[nodiscard]] const SectionVector &stdSummarySections() const { return s_stdSummarySections; } + [[nodiscard]] const SectionVector &stdDetailsSections() const { return s_stdDetailsSections; } + [[nodiscard]] const SectionVector &stdCppClassSummarySections() const + { + return s_stdCppClassSummarySections; + } + [[nodiscard]] const SectionVector &stdCppClassDetailsSections() const + { + return s_stdCppClassDetailsSections; + } + [[nodiscard]] const SectionVector &stdQmlTypeSummarySections() const + { + return s_stdQmlTypeSummarySections; + } + [[nodiscard]] const SectionVector &stdQmlTypeDetailsSections() const + { + return s_stdQmlTypeDetailsSections; + } + + [[nodiscard]] Aggregate *aggregate() const { return m_aggregate; } + +private: + void stdRefPageSwitch(SectionVector &v, Node *n, Node *t = nullptr); + void distributeNodeInSummaryVector(SectionVector &sv, Node *n); + void distributeNodeInDetailsVector(SectionVector &dv, Node *n); + void distributeQmlNodeInDetailsVector(SectionVector &dv, Node *n); + void distributeQmlNodeInSummaryVector(SectionVector &sv, Node *n, bool sharing = false); + void initAggregate(SectionVector &v, Aggregate *aggregate); + +private: + Aggregate *m_aggregate { nullptr }; + + static SectionVector s_stdSummarySections; + static SectionVector s_stdDetailsSections; + static SectionVector s_stdCppClassSummarySections; + static SectionVector s_stdCppClassDetailsSections; + static SectionVector s_stdQmlTypeSummarySections; + static SectionVector s_stdQmlTypeDetailsSections; + static SectionVector s_sinceSections; + static SectionVector s_allMembers; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/sharedcommentnode.cpp b/src/qdoc/qdoc/src/qdoc/sharedcommentnode.cpp new file mode 100644 index 000000000..05204d1f3 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/sharedcommentnode.cpp @@ -0,0 +1,56 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "sharedcommentnode.h" + +#include "aggregate.h" +#include "functionnode.h" +#include "qmltypenode.h" + +QT_BEGIN_NAMESPACE + +SharedCommentNode::SharedCommentNode(QmlTypeNode *parent, int count, QString &group) + : Node(Node::SharedComment, parent, group) +{ + m_collective.reserve(count); +} + +/*! + Searches the shared comment node's member nodes for function + nodes. Each function node's overload flag is set. + */ +void SharedCommentNode::setOverloadFlags() +{ + for (auto *node : m_collective) { + if (node->isFunction()) + static_cast<FunctionNode *>(node)->setOverloadFlag(); + } +} + +/*! + Clone this node on the heap and make the clone a child of + \a parent. + + Returns the pointer to the clone. + */ +Node *SharedCommentNode::clone(Aggregate *parent) +{ + auto *scn = new SharedCommentNode(*this); // shallow copy + scn->setParent(nullptr); + parent->addChild(scn); + + return scn; +} + +/*! + Sets the related nonmember flag in this node and in each + node in the shared comment's collective to \a value. + */ +void SharedCommentNode::setRelatedNonmember(bool value) +{ + Node::setRelatedNonmember(value); + for (auto *node : m_collective) + node->setRelatedNonmember(value); +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/sharedcommentnode.h b/src/qdoc/qdoc/src/qdoc/sharedcommentnode.h new file mode 100644 index 000000000..d319d7c59 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/sharedcommentnode.h @@ -0,0 +1,51 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef SHAREDCOMMENTNODE_H +#define SHAREDCOMMENTNODE_H + +#include "node.h" + +#include <QtCore/qglobal.h> +#include <QtCore/qlist.h> + +QT_BEGIN_NAMESPACE + +class Aggregate; +class QmlTypeNode; + +class SharedCommentNode : public Node +{ +public: + explicit SharedCommentNode(Node *node) : Node(Node::SharedComment, node->parent(), QString()) + { + m_collective.reserve(1); + append(node); + } + SharedCommentNode(QmlTypeNode *parent, int count, QString &group); + ~SharedCommentNode() override { m_collective.clear(); } + + [[nodiscard]] bool isPropertyGroup() const override + { + return !name().isEmpty() && !m_collective.isEmpty() && (m_collective.at(0)->isQmlProperty()); + } + [[nodiscard]] qsizetype count() const { return m_collective.size(); } + void append(Node *node) + { + m_collective.append(node); + node->setSharedCommentNode(this); + setGenus(node->genus()); + } + void sort() { std::sort(m_collective.begin(), m_collective.end(), Node::nodeNameLessThan); } + [[nodiscard]] const QList<Node *> &collective() const { return m_collective; } + void setOverloadFlags(); + void setRelatedNonmember(bool value) override; + Node *clone(Aggregate *parent) override; + +private: + QList<Node *> m_collective {}; +}; + +QT_END_NAMESPACE + +#endif // SHAREDCOMMENTNODE_H diff --git a/src/qdoc/qdoc/src/qdoc/singleton.h b/src/qdoc/qdoc/src/qdoc/singleton.h new file mode 100644 index 000000000..085a5ea64 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/singleton.h @@ -0,0 +1,32 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +#ifndef SINGLETON_H +#define SINGLETON_H + +QT_BEGIN_NAMESPACE + +/*! + \class Singleton + \internal + + Class template for singleton objects in QDoc. + */ +template<typename T> +class Singleton +{ +public: + Singleton(const Singleton &) = delete; + Singleton &operator=(const Singleton &) = delete; + static T &instance() + { + static T s_instance {}; + return s_instance; + } + +protected: + Singleton() = default; +}; + +QT_END_NAMESPACE + +#endif // SINGLETON_H diff --git a/src/qdoc/qdoc/src/qdoc/sourcefileparser.h b/src/qdoc/qdoc/src/qdoc/sourcefileparser.h new file mode 100644 index 000000000..95bc71627 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/sourcefileparser.h @@ -0,0 +1,82 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "clangcodeparser.h" +#include "puredocparser.h" + +#include <variant> + +#include <QString> +#include <QFileInfo> + +struct CppSourceFile {}; +struct QDocSourceFile {}; +struct JsSourceFile {}; +struct UnknownSourceFile {}; + +using SourceFileTag = std::variant<CppSourceFile, QDocSourceFile, JsSourceFile, UnknownSourceFile>; +using TaggedSourceFile = std::pair<QString, SourceFileTag>; + +inline TaggedSourceFile tag_source_file(const QString& path) { + static QStringList cpp_file_extensions{ + "c++", "cc", "cpp", "cxx", "mm" + }; + static QStringList qdoc_file_extensions{ "qdoc" }; + static QStringList javascript_file_extensions{ "js" }; + + QString extension{QFileInfo(path).suffix()}; + + if (cpp_file_extensions.contains(extension)) return TaggedSourceFile{path, CppSourceFile{}}; + else if (qdoc_file_extensions.contains(extension)) return TaggedSourceFile{path, QDocSourceFile{}}; + else if (javascript_file_extensions.contains(extension)) return TaggedSourceFile{path, JsSourceFile{}}; + + return TaggedSourceFile{path, UnknownSourceFile{}}; +} + +class SourceFileParser { +public: + struct ParseResult { + std::vector<UntiedDocumentation> untied; + std::vector<TiedDocumentation> tied; + }; + +public: + SourceFileParser(ClangCodeParser& clang_parser, PureDocParser& pure_parser) + : cpp_file_parser(clang_parser), + pure_file_parser(pure_parser) + {} + + ParseResult operator()(const TaggedSourceFile& source) { + if (std::holds_alternative<CppSourceFile>(source.second)) + return (*this)(source.first, std::get<CppSourceFile>(source.second)); + else if (std::holds_alternative<QDocSourceFile>(source.second)) + return (*this)(source.first, std::get<QDocSourceFile>(source.second)); + else if (std::holds_alternative<JsSourceFile>(source.second)) + return (*this)(source.first, std::get<JsSourceFile>(source.second)); + else if (std::holds_alternative<UnknownSourceFile>(source.second)) + return {{}, {}}; + + Q_UNREACHABLE(); + } + +private: + ParseResult operator()(const QString& path, CppSourceFile) { + auto [untied, tied] = cpp_file_parser.parse_cpp_file(path); + + return {untied, tied}; + } + + ParseResult operator()(const QString& path, QDocSourceFile) { + return {pure_file_parser.parse_qdoc_file(path), {}}; + } + + ParseResult operator()(const QString& path, JsSourceFile) { + return {pure_file_parser.parse_qdoc_file(path), {}}; + } + +private: + ClangCodeParser& cpp_file_parser; + PureDocParser& pure_file_parser; +}; diff --git a/src/qdoc/qdoc/src/qdoc/tagfilewriter.cpp b/src/qdoc/qdoc/src/qdoc/tagfilewriter.cpp new file mode 100644 index 000000000..7b6b496ca --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/tagfilewriter.cpp @@ -0,0 +1,306 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "tagfilewriter.h" + +#include "access.h" +#include "aggregate.h" +#include "classnode.h" +#include "enumnode.h" +#include "functionnode.h" +#include "htmlgenerator.h" +#include "location.h" +#include "node.h" +#include "propertynode.h" +#include "qdocdatabase.h" +#include "typedefnode.h" + +QT_BEGIN_NAMESPACE + +/*! + \class TagFileWriter + + This class handles the generation of the QDoc tag files. + */ + +/*! + Default constructor. \a qdb is the pointer to the + qdoc database that is used when reading and writing the + index files. + */ +TagFileWriter::TagFileWriter() : m_qdb(QDocDatabase::qdocDB()) { } + +/*! + Generate the tag file section with the given \a writer for the \a parent + node. + */ +void TagFileWriter::generateTagFileCompounds(QXmlStreamWriter &writer, const Aggregate *parent) +{ + const auto &nonFunctionList = const_cast<Aggregate *>(parent)->nonfunctionList(); + for (const auto *node : nonFunctionList) { + if (!node->url().isNull() || node->isPrivate()) + continue; + + QString kind; + switch (node->nodeType()) { + case Node::Namespace: + kind = "namespace"; + break; + case Node::Class: + case Node::Struct: + case Node::Union: + case Node::QmlType: + kind = "class"; + break; + default: + continue; + } + const auto *aggregate = static_cast<const Aggregate *>(node); + + QString access = "public"; + if (node->isProtected()) + access = "protected"; + + QString objName = node->name(); + + // Special case: only the root node should have an empty name. + if (objName.isEmpty() && node != m_qdb->primaryTreeRoot()) + continue; + + // *** Write the starting tag for the element here. *** + writer.writeStartElement("compound"); + writer.writeAttribute("kind", kind); + + if (node->isClassNode()) { + writer.writeTextElement("name", node->fullDocumentName()); + writer.writeTextElement("filename", m_generator->fullDocumentLocation(node)); + + // Classes contain information about their base classes. + const auto *classNode = static_cast<const ClassNode *>(node); + const QList<RelatedClass> &bases = classNode->baseClasses(); + for (const auto &related : bases) { + ClassNode *n = related.m_node; + if (n) + writer.writeTextElement("base", n->name()); + } + + // Recurse to write all members. + generateTagFileMembers(writer, aggregate); + writer.writeEndElement(); + + // Recurse to write all compounds. + generateTagFileCompounds(writer, aggregate); + } else { + writer.writeTextElement("name", node->fullDocumentName()); + writer.writeTextElement("filename", m_generator->fullDocumentLocation(node)); + + // Recurse to write all members. + generateTagFileMembers(writer, aggregate); + writer.writeEndElement(); + + // Recurse to write all compounds. + generateTagFileCompounds(writer, aggregate); + } + } +} + +/*! + Writes all the members of the \a parent node with the \a writer. + The node represents a C++ class, namespace, etc. + */ +void TagFileWriter::generateTagFileMembers(QXmlStreamWriter &writer, const Aggregate *parent) +{ + auto childNodes = parent->childNodes(); + std::sort(childNodes.begin(), childNodes.end(), Node::nodeNameLessThan); + for (const auto *node : childNodes) { + if (!node->url().isNull()) + continue; + + QString nodeName; + QString kind; + switch (node->nodeType()) { + case Node::Enum: + nodeName = "member"; + kind = "enumeration"; + break; + case Node::TypeAlias: // Treated as typedef + case Node::Typedef: + nodeName = "member"; + kind = "typedef"; + break; + case Node::Property: + nodeName = "member"; + kind = "property"; + break; + case Node::Function: + nodeName = "member"; + kind = "function"; + break; + case Node::Namespace: + nodeName = "namespace"; + break; + case Node::Class: + case Node::Struct: + case Node::Union: + nodeName = "class"; + break; + case Node::Variable: + default: + continue; + } + + QString access; + switch (node->access()) { + case Access::Public: + access = "public"; + break; + case Access::Protected: + access = "protected"; + break; + case Access::Private: + default: + continue; + } + + QString objName = node->name(); + + // Special case: only the root node should have an empty name. + if (objName.isEmpty() && node != m_qdb->primaryTreeRoot()) + continue; + + // *** Write the starting tag for the element here. *** + writer.writeStartElement(nodeName); + if (!kind.isEmpty()) + writer.writeAttribute("kind", kind); + + switch (node->nodeType()) { + case Node::Class: + case Node::Struct: + case Node::Union: + writer.writeCharacters(node->fullDocumentName()); + writer.writeEndElement(); + break; + case Node::Namespace: + writer.writeCharacters(node->fullDocumentName()); + writer.writeEndElement(); + break; + case Node::Function: { + /* + Function nodes contain information about + the type of function being described. + */ + + const auto *functionNode = static_cast<const FunctionNode *>(node); + writer.writeAttribute("protection", access); + writer.writeAttribute("virtualness", functionNode->virtualness()); + writer.writeAttribute("static", functionNode->isStatic() ? "yes" : "no"); + + if (functionNode->isNonvirtual()) + writer.writeTextElement("type", functionNode->returnType()); + else + writer.writeTextElement("type", "virtual " + functionNode->returnType()); + + writer.writeTextElement("name", objName); + const QStringList pieces = + m_generator->fullDocumentLocation(node).split(QLatin1Char('#')); + writer.writeTextElement("anchorfile", pieces[0]); + writer.writeTextElement("anchor", pieces[1]); + QString signature = functionNode->signature(Node::SignatureReturnType); + signature = signature.mid(signature.indexOf(QChar('('))).trimmed(); + if (functionNode->isConst()) + signature += " const"; + if (functionNode->isFinal()) + signature += " final"; + if (functionNode->isOverride()) + signature += " override"; + if (functionNode->isPureVirtual()) + signature += " = 0"; + writer.writeTextElement("arglist", signature); + } + writer.writeEndElement(); // member + break; + case Node::Property: { + const auto *propertyNode = static_cast<const PropertyNode *>(node); + writer.writeAttribute("type", propertyNode->dataType()); + writer.writeTextElement("name", objName); + const QStringList pieces = + m_generator->fullDocumentLocation(node).split(QLatin1Char('#')); + writer.writeTextElement("anchorfile", pieces[0]); + writer.writeTextElement("anchor", pieces[1]); + writer.writeTextElement("arglist", QString()); + } + writer.writeEndElement(); // member + break; + case Node::Enum: { + const auto *enumNode = static_cast<const EnumNode *>(node); + writer.writeTextElement("name", objName); + const QStringList pieces = + m_generator->fullDocumentLocation(node).split(QLatin1Char('#')); + writer.writeTextElement("anchorfile", pieces[0]); + writer.writeTextElement("anchor", pieces[1]); + writer.writeEndElement(); // member + + for (const auto &item : enumNode->items()) { + writer.writeStartElement("member"); + writer.writeAttribute("kind", "enumvalue"); + writer.writeTextElement("name", item.name()); + writer.writeTextElement("anchorfile", pieces[0]); + writer.writeTextElement("anchor", pieces[1]); + writer.writeTextElement("arglist", QString()); + writer.writeEndElement(); // member + } + } break; + case Node::TypeAlias: // Treated as typedef + case Node::Typedef: { + const auto *typedefNode = static_cast<const TypedefNode *>(node); + if (typedefNode->associatedEnum()) + writer.writeAttribute("type", typedefNode->associatedEnum()->fullDocumentName()); + else + writer.writeAttribute("type", QString()); + writer.writeTextElement("name", objName); + const QStringList pieces = + m_generator->fullDocumentLocation(node).split(QLatin1Char('#')); + writer.writeTextElement("anchorfile", pieces[0]); + writer.writeTextElement("anchor", pieces[1]); + writer.writeTextElement("arglist", QString()); + } + writer.writeEndElement(); // member + break; + + case Node::Variable: + default: + break; + } + } +} + +/*! + Writes a tag file named \a fileName. + */ +void TagFileWriter::generateTagFile(const QString &fileName, Generator *g) +{ + QFile file(fileName); + QFileInfo fileInfo(fileName); + + // If no path was specified or it doesn't exist, + // default to the output directory + if (fileInfo.fileName() == fileName || !fileInfo.dir().exists()) + file.setFileName(m_generator->outputDir() + QLatin1Char('/') + fileInfo.fileName()); + + if (!file.open(QFile::WriteOnly | QFile::Text)) { + Location().warning(QString("Failed to open %1 for writing.").arg(file.fileName())); + return; + } + + m_generator = g; + QXmlStreamWriter writer(&file); + writer.setAutoFormatting(true); + writer.writeStartDocument(); + writer.writeStartElement("tagfile"); + generateTagFileCompounds(writer, m_qdb->primaryTreeRoot()); + writer.writeEndElement(); // tagfile + writer.writeEndDocument(); + file.close(); +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/tagfilewriter.h b/src/qdoc/qdoc/src/qdoc/tagfilewriter.h new file mode 100644 index 000000000..40a1ed399 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/tagfilewriter.h @@ -0,0 +1,33 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef TAGFILEWRITER_H +#define TAGFILEWRITER_H + +#include <QtCore/qxmlstream.h> + +QT_BEGIN_NAMESPACE + +class Aggregate; +class Generator; +class QDocDatabase; + +class TagFileWriter +{ +public: + TagFileWriter(); + ~TagFileWriter() = default; + + void generateTagFile(const QString &fileName, Generator *generator); + +private: + void generateTagFileCompounds(QXmlStreamWriter &writer, const Aggregate *inner); + void generateTagFileMembers(QXmlStreamWriter &writer, const Aggregate *inner); + + QDocDatabase *m_qdb { nullptr }; + Generator *m_generator { nullptr }; +}; + +QT_END_NAMESPACE + +#endif // TAGFILEWRITER_H diff --git a/src/qdoc/qdoc/src/qdoc/template_declaration.h b/src/qdoc/qdoc/src/qdoc/template_declaration.h new file mode 100644 index 000000000..a0aade682 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/template_declaration.h @@ -0,0 +1,502 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include <algorithm> +#include <cstdint> +#include <functional> +#include <numeric> +#include <optional> +#include <string> +#include <vector> + +#include <QString> + +/* + * Represents a general declaration that has a form that can be + * described by a type, name and initializer triplet, or any such form + * that can be described by zero or more of those same parts. + * + * For example, it can be used to represent a C++ variable declaration + * such as: + * + * std::vector<int> foo = { 1, 2, 3 }; + * + * Where `std::vector<int>` is the type, `foo` is the name and `{ 1, 2, + * 3 }` is the initializer. + * + * Similarly, it can be used to represent a non-type template parameter + * declaration, such as the `foo` parameter in: + * + * template<int foo = 10> + * + * Where `int` is the type, `foo` is the name and `10` is the + * initializer. + * + * An instance can be used to represent less information dense elements + * by setting one or more of the fields as the empty string. + * + * For example, a template type parameter such as `T` in: + * + * template<typename T = int> + * + * Can be represented by an instance that has an empty string as the + * type, `T` as the name and `int` as the initializer. + * + * In general, it can be used to represent any such element that has + * zero or more of the three components, albeit, in QDoc, it is + * specifically intended to be used to represent various C++ + * declarations. + * + * All three fields are lowered stringified version of the original + * declaration, so that the type should be used at the end of a + * pipeline where the semantic property of the represented code are not + * required. + */ +struct ValuedDeclaration +{ + struct PrintingPolicy + { + bool include_type = true; + bool include_name = true; + bool include_initializer = true; + }; + + std::string type; + std::string name; + std::string initializer; + + // KLUDGE: Workaround for + // https://stackoverflow.com/questions/53408962/try-to-understand-compiler-error-message-default-member-initializer-required-be + static PrintingPolicy default_printing_policy() { return PrintingPolicy{}; } + + /* + * Constructs and returns a human-readable representation of this + * declaration. + * + * The constructed string is formatted so that as to rebuild a + * possible version of the C++ code that is modeled by an instance + * of this type. + * + * Each component participates in the human-presentable version if + * it they are not the empty string. + * + * The "type" and "name" component participate with their literal + * representation. + * + * The "iniitlalizer" components contributes an equal symbol, + * followed by a space followed by the literal representation of + * the component. + * + * The component contributes in an ordered way, with "type" + * contributing first, "name" contributing second and + * "initializer" contributing last. + * + * Each contribution is separated by a space if the component that + * comes before it, if any, has contributed to the human-readable + * representation. + * + * For example, an instance of this type that has "type" component + * "int", "name" component "foo" and "iniitializer" component + * "100", would be represented as: + * + * int foo = 100 + * + * Where "int" is the "type" component contribution, "foo" is the + * "name" component contribution and "= 100" is the "initializer" + * component contribution. + * Each of those contribution is separated by a space, as each + * "preceding" component has contributed to the representation. + * + * If we provide a similar instance with, for example, the "type" + * and "name" components as the empty string, then the + * representation would be "= 100", which is the "initializer" + * component contribution, the only component that is not the + * empty string. + * + * The policy argument allows to treat certain components as if + * they were the empty string. + * + * For example, given an instance of this type that has "type" + * component "double", "name" component "bar" and "iniitializer" + * component "10.2", its human-readable representation would be + * "double bar = 10.2". + * + * If the representation of that same instance was obtained by + * using a policy that excludes the "name" component, then that + * representation would be "double = 10.2", which is equivalent + * to the representation of an instance that is the same as the + * orginal one with the "name" component as the empty string. + */ + inline std::string to_std_string(PrintingPolicy policy = default_printing_policy()) const + { + std::string s{}; + + if (!type.empty() && policy.include_type) + s += (s.empty() ? "" : " ") + type; + + if (!name.empty() && policy.include_name) + s += (s.empty() ? "" : " ") + name; + + if (!initializer.empty() && policy.include_initializer) + s += (s.empty() ? "= " : " = ") + initializer; + + return s; + } +}; + +struct RelaxedTemplateParameter; + +struct TemplateDeclarationStorage +{ + std::vector<RelaxedTemplateParameter> parameters; + + inline std::string to_std_string() const; +}; + +/* + * Represents a C++ template parameter. + * + * The model used by this representation is a slighly simplified + * model. + * + * In the model, template parameters are one of: + * + * - A type template parameter. + * - A non type template parameter. + * - A template template parameter. + * + * Furthermore, each parameter can: + * + * - Be a parameter pack. + * - Carry an additional template declaration (as a template template + * parameter would). + * - Have no declared type. + * - Have no declared name. + * - Have no declared initializer. + * + * Due to this simplified model certain incorrect parameters can be + * represented. + * + * For example, it might be possible to represent a parameter pack + * that has a default initializer, a non-type template parameter that + * has no type or a template template parameter that carries no + * template declaration. + * + * The model further elides some of the semantic that might be carried + * by a parameter. + * For example, the model has no specific concept for template + * constraints. + * + * Template parameters can be represented as instances of the type. + * + * For example, a type template parameter `typename T` can be + * represented as the following instance: + * + * RelaxedTemplateParameter{ + * RelaxedTemplateParameter::Kind::TypeTemplateParameter, + * false, + * { + * "", + * "T", + * "" + * }, + * {} + * }; + * + * And a non-type template parameter pack "int... Args" as: + * + * RelaxedTemplateParameter{ + * RelaxedTemplateParameter::Kind::NonTypeTemplateParameter, + * true, + * { + * "int", + * "Args", + * "" + * }, + * {} + * }; + * + * Due to the relaxed constraint and the representable incorrect + * parameters, the type is intended to be used for data that is + * already validated and known to be correct, such as data that is + * extracted from Clang. + */ +struct RelaxedTemplateParameter +{ + enum class Kind : std::uint8_t { + TypeTemplateParameter, + NonTypeTemplateParameter, + TemplateTemplateParameter + }; + + Kind kind; + bool is_parameter_pack; + ValuedDeclaration valued_declaration; + std::optional<TemplateDeclarationStorage> template_declaration; + + /* + * Constructs and returns a human-readable representation of this + * parameter. + * + * The constructed string is formatted so that as to rebuild a + * possible version of the C++ code that is modeled by an instance + * of this type. + * + * The format of the representation varies based on the "kind" of + * the parameter. + * + * - A "TypeTemplateParameter", is constructed as the + * concatenation of the literal "typename", followed by the + * literal "..." if the parameter is a pack, followed by the + * human-readable representaion of "valued_declaration". + * + * If the human-readable representation of + * "valued_declaration" is not the empty string, it is + * preceded by a space when it contributes to the + * representation. + * + * For example, the C++ type template parameter "typename Foo + * = int", would be represented by the instance: + * + * RelaxedTemplateParameter{ + * RelaxedTemplateParameter::Kind::TypeTemplateParameter, + * false, + * { + * "", + * "Foo", + * "int" + * }, + * {} + * }; + * + * And its representation would be: + * + * typename Foo = int + * + * Where "typename" is the added literal and "Foo = int" is + * the representation for "valued_declaration", with a space + * in-between the two contributions. + * + * - A "NonTypeTemplateParameter", is constructed by the + * contribution of the "type" compoment of "valued_declaration", + * followed by the literal "..." if the parameter is a pack, + * followed by the human-presentable version of + * "valued_declaration" without its "type" component + * contribution. + * + * If the contribution of the "type" component of + * "valued_declaration" is not empty, the next contribution is + * preceded by a space. + * + * For example, the C++ non-type template parameter "int... + * SIZE", would be represented by the instance: + * + * + * RelaxedTemplateParameter{ + * RelaxedTemplateParameter::Kind::NonTypeTemplateParameter, + * true, + * { + * "int", + * "SIZE", + * "" + * }, + * {} + * }; + * + * And its representation would be: + * + * int... SIZE + * + * Where "int" is the "type" component contribution of + * "valued_declaration", "..." is the added literal due to + * the parameter being a pack and " SIZE" being the + * human-readable representation of "valued_declaration" + * without its "type" component contribution, preceded by a + * space. + * + * - A "TemplateTemplateParameter", is constructed by the + * contribution of the human-presentable representation of + * "template_declaration", followed by the representation of + * this parameter if it was a "TypeTemplateParameter", with a + * space between the two contributions if the + * human-presentable representation of "template_declaration" + * is not empty. + * + * For example, the C++ template template template parameter + * "template<typename> T", would be represented by the + * instance: + * + * + * RelaxedTemplateParameter{ + * RelaxedTemplateParameter::Kind::TemplateTemplateParameter, + * false, + * { + * "", + * "T", + * "" + * }, + * { + * RelaxedTemplateParameter{ + * RelaxedTemplateParameter::Kind::TypeTemplateParameter, + * false, + * { + * "", + * "", + * "" + * }, + * {} + * } + * } + * }; + * + * And its representation would be: + * + * template <typename> typename T + * + * Where "template <typename>" human-presentable version of + * "template_declaration" and "typename T" is the + * human-presentable version of this parameter if it was a + * type template parameter. + * + * With a space between the two contributions. + */ + inline std::string to_std_string() const + { + switch (kind) { + // TODO: This can probably be moved under the template + // template parameter case and reused through a fallback. + case Kind::TypeTemplateParameter: { + std::string valued_declaration_string = valued_declaration.to_std_string(); + + return std::string("typename") + (is_parameter_pack ? "..." : "") + + (valued_declaration_string.empty() ? "" : " ") + valued_declaration_string; + } + case Kind::NonTypeTemplateParameter: { + std::string type_string = valued_declaration.type + (is_parameter_pack ? "..." : ""); + + return type_string + (type_string.empty() ? "" : " ") + + valued_declaration.to_std_string( + ValuedDeclaration::PrintingPolicy{ false, true, true }); + } + case Kind::TemplateTemplateParameter: { + std::string valued_declaration_string = valued_declaration.to_std_string(); + + return (template_declaration ? (*template_declaration).to_std_string() + " " : "") + + "typename" + (is_parameter_pack ? "..." : "") + + (valued_declaration_string.empty() ? "" : " ") + valued_declaration_string; + } + default: + return ""; + } + } +}; + +/* + * Represents a C++ template declaration as a collection of template + * parameters. + * + * The parameters for the declaration follow the same relaxed rules as + * `RelaxedTemplateParameter` and inherit the possibility of + * representing incorrect declarations. + * + * Due to the relaxed constraint and the representable incorrect + * parameters, the type is intended to be used for data that is + * already validated and known to be correct, such as data that is + * extracted from Clang. + */ +struct RelaxedTemplateDeclaration : TemplateDeclarationStorage +{ + inline QString to_qstring() const { return QString::fromStdString(to_std_string()); } +}; + +/* + * Constructs and returns a human-readable representation of this + * declaration. + * + * The constructed string is formatted so as to rebuild a + * possible version of the C++ code that is modeled by an instance + * of this type. + * + * The representation of a declaration is constructed by the literal + * "template <", followed by the human-presentable version of each + * parameter in "parameters", with a comma and a space between each + * parameter, followed by a closing literal ">". + * + * For example, the empty declaration is represented as "template <>". + * + * While a template declaration that has a type template parameter + * "Foo" with initializer "int" and a non-type template parameter pack + * with type "int" and name "S" would be represented as: + * + * template <typename Foo = int, int... S> + */ +inline std::string TemplateDeclarationStorage::to_std_string() const +{ + if (parameters.empty()) + return "template <>"; + + return "template <" + + std::accumulate(std::next(parameters.cbegin()), parameters.cend(), + parameters.front().to_std_string(), + [](auto &&acc, const RelaxedTemplateParameter ¶meter) { + return acc + ", " + parameter.to_std_string(); + }) + + ">"; +} + +/* + * Returns true if the two template declaration represented by left + * and right are substitutable. + * + * QDoc uses a simplified model for template declarations and, + * similarly, uses a simplified model of "substitutability". + * + * Two declarations are substitutable if: + * + * - They have the same amount of parameters + * - For each pair of parameters with the same postion: + * - They have the same kind + * - They are both parameter packs or both are not parameter packs + * - If they are non-type template parameters then they have the same type + * - If they are both template template parameters then they both + * carry an additional template declaration and the additional + * template declarations are substitutable + * + * This means that in the simplified models, we generally ignore default arguments, name and such. + * + * This model does not follow the way C++ performs disambiguation but + * should be enough to handle most cases in the documentation. + */ +inline bool are_template_declarations_substitutable(const TemplateDeclarationStorage& left, const TemplateDeclarationStorage& right) { + static auto are_template_parameters_substitutable = [](const RelaxedTemplateParameter& left, const RelaxedTemplateParameter& right) { + if (left.kind != right.kind) return false; + if (left.is_parameter_pack != right.is_parameter_pack) return false; + + if (left.kind == RelaxedTemplateParameter::Kind::NonTypeTemplateParameter && + (left.valued_declaration.type != right.valued_declaration.type)) + return false; + + if (left.kind == RelaxedTemplateParameter::Kind::TemplateTemplateParameter) { + if (!left.template_declaration && right.template_declaration) return false; + if (left.template_declaration && !right.template_declaration) return false; + + if (left.template_declaration && right.template_declaration) + return are_template_declarations_substitutable(*left.template_declaration, *right.template_declaration); + } + + return true; + }; + + const auto& left_parameters = left.parameters; + const auto& right_parameters = right.parameters; + + if (left_parameters.size() != right_parameters.size()) return false; + + return std::transform_reduce(left_parameters.cbegin(), left_parameters.cend(), right_parameters.cbegin(), + true, + std::logical_and<bool>{}, + are_template_parameters_substitutable + ); +} diff --git a/src/qdoc/qdoc/src/qdoc/text.cpp b/src/qdoc/qdoc/src/qdoc/text.cpp new file mode 100644 index 000000000..d3401ce68 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/text.cpp @@ -0,0 +1,341 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "text.h" + +#include <QtCore/qregularexpression.h> + +#include <cstdio> + +QT_BEGIN_NAMESPACE + +Text::Text() : m_first(nullptr), m_last(nullptr) { } + +Text::Text(const QString &str) : m_first(nullptr), m_last(nullptr) +{ + operator<<(str); +} + +Text::Text(const Text &text) : m_first(nullptr), m_last(nullptr) +{ + operator=(text); +} + +Text::~Text() +{ + clear(); +} + +Text &Text::operator=(const Text &text) +{ + if (this != &text) { + clear(); + operator<<(text); + } + return *this; +} + +Text &Text::operator<<(Atom::AtomType atomType) +{ + return operator<<(Atom(atomType)); +} + +Text &Text::operator<<(const QString &string) +{ + return string.isEmpty() ? *this : operator<<(Atom(Atom::String, string)); +} + +Text &Text::operator<<(const Atom &atom) +{ + if (atom.count() < 2) { + if (m_first == nullptr) { + m_first = new Atom(atom.type(), atom.string()); + m_last = m_first; + } else + m_last = new Atom(m_last, atom.type(), atom.string()); + } else { + if (m_first == nullptr) { + m_first = new Atom(atom.type(), atom.string(), atom.string(1)); + m_last = m_first; + } else + m_last = new Atom(m_last, atom.type(), atom.string(), atom.string(1)); + } + return *this; +} + +/*! + Special output operator for LinkAtom. It makes a copy of + the LinkAtom \a atom and connects the cop;y to the list + in this Text. + */ +Text &Text::operator<<(const LinkAtom &atom) +{ + if (m_first == nullptr) { + m_first = new LinkAtom(atom); + m_last = m_first; + } else + m_last = new LinkAtom(m_last, atom); + return *this; +} + +Text &Text::operator<<(const Text &text) +{ + const Atom *atom = text.firstAtom(); + while (atom != nullptr) { + operator<<(*atom); + atom = atom->next(); + } + return *this; +} + +void Text::stripFirstAtom() +{ + if (m_first != nullptr) { + if (m_first == m_last) + m_last = nullptr; + Atom *oldFirst = m_first; + m_first = m_first->next(); + delete oldFirst; + } +} + +void Text::stripLastAtom() +{ + if (m_last != nullptr) { + Atom *oldLast = m_last; + if (m_first == m_last) { + m_first = nullptr; + m_last = nullptr; + } else { + m_last = m_first; + while (m_last->next() != oldLast) + m_last = m_last->next(); + m_last->setNext(nullptr); + } + delete oldLast; + } +} + +/*! + This function traverses the atom list of the Text object, + extracting all the string parts. It concatenates them to + a result string and returns it. + */ +QString Text::toString() const +{ + QString str; + const Atom *atom = firstAtom(); + while (atom != nullptr) { + if (atom->type() == Atom::String || atom->type() == Atom::AutoLink + || atom->type() == Atom::C) + str += atom->string(); + atom = atom->next(); + } + return str; +} + +/*! + Returns true if this Text contains the substring \a str. + */ +bool Text::contains(const QString &str) const +{ + const Atom *atom = firstAtom(); + while (atom != nullptr) { + if (atom->type() == Atom::String || atom->type() == Atom::AutoLink + || atom->type() == Atom::C) + if (atom->string().contains(str, Qt::CaseInsensitive)) + return true; + atom = atom->next(); + } + return false; +} + +Text Text::subText(Atom::AtomType left, Atom::AtomType right, const Atom *from, + bool inclusive) const +{ + const Atom *begin = from ? from : firstAtom(); + const Atom *end; + + while (begin != nullptr && begin->type() != left) + begin = begin->next(); + if (begin != nullptr) { + if (!inclusive) + begin = begin->next(); + } + + end = begin; + while (end != nullptr && end->type() != right) + end = end->next(); + if (end == nullptr) + begin = nullptr; + else if (inclusive) + end = end->next(); + return subText(begin, end); +} + +Text Text::sectionHeading(const Atom *sectionLeft) +{ + if (sectionLeft != nullptr) { + const Atom *begin = sectionLeft; + while (begin != nullptr && begin->type() != Atom::SectionHeadingLeft) + begin = begin->next(); + if (begin != nullptr) + begin = begin->next(); + + const Atom *end = begin; + while (end != nullptr && end->type() != Atom::SectionHeadingRight) + end = end->next(); + + if (end != nullptr) + return subText(begin, end); + } + return Text(); +} + +/*! + Prints a human-readable version of the contained atoms to stderr. + + The output is formatted as a linear list of atoms, with each atom + being on its own line. + + Each atom is represented by its type and its stringified-contents, + if any, with a space between the two. + + Indentation is used to emphasize the possible block-level + relationship between consecutive atoms, increasing after a + "Left" atom and decreasing just before a "Right" atom. + + For example, if this `Text` represented the block-comment + containing the text: + + \c {\l {somelink} {This is a link}} + + Then the human-readable output would look like the following: + + \badcode + ParaLeft + Link "somelink" + FormattingLeft "link" + String "This is a link" + FormattingRight "link" + String + ParaRight + \endcode + */ +void Text::dump() const +{ + constexpr int minimum_indentation_level { 1 }; + int indentation_level { minimum_indentation_level }; + int indentation_width { 4 }; + + const Atom *atom = firstAtom(); + while (atom != nullptr) { + QString str = atom->string(); + str.replace("\\", "\\\\"); + str.replace("\"", "\\\""); + str.replace("\n", "\\n"); + static const QRegularExpression re(R"([^ -~])"); + str.replace(re, "?"); + if (!str.isEmpty()) + str = " \"" + str + QLatin1Char('"'); + + QString atom_type = atom->typeString(); + if (atom_type.contains("Right")) + indentation_level = std::max(minimum_indentation_level, indentation_level - 1); + + fprintf(stderr, "%s%s%s\n", + QString(indentation_level * indentation_width, ' ').toLatin1().data(), + atom_type.toLatin1().data(), str.toLatin1().data()); + + if (atom_type.contains("Left")) + indentation_level += 1; + + atom = atom->next(); + } +} + +Text Text::subText(const Atom *begin, const Atom *end) +{ + Text text; + if (begin != nullptr) { + while (begin != end) { + text << *begin; + begin = begin->next(); + } + } + return text; +} + +void Text::clear() +{ + while (m_first != nullptr) { + Atom *atom = m_first; + m_first = m_first->next(); + delete atom; + } + m_first = nullptr; + m_last = nullptr; +} + +int Text::compare(const Text &text1, const Text &text2) +{ + if (text1.isEmpty()) + return text2.isEmpty() ? 0 : -1; + if (text2.isEmpty()) + return 1; + + const Atom *atom1 = text1.firstAtom(); + const Atom *atom2 = text2.firstAtom(); + + for (;;) { + if (atom1->type() != atom2->type()) + return (int)atom1->type() - (int)atom2->type(); + int cmp = QString::compare(atom1->string(), atom2->string()); + if (cmp != 0) + return cmp; + + if (atom1 == text1.lastAtom()) + return atom2 == text2.lastAtom() ? 0 : -1; + if (atom2 == text2.lastAtom()) + return 1; + atom1 = atom1->next(); + atom2 = atom2->next(); + } +} + +/*! + \internal + + \brief Splits the current Text from \a start to end into a new Text object. + + Returns a new Text from the first Atom in this Text of atom type \a start. + */ +Text Text::splitAtFirst(Atom::AtomType start) { + if (m_first == nullptr) + return {}; + + Atom *previous = nullptr; + Atom *current = m_first; + + while (current != nullptr) { + if (current->type() == start) + break; + previous = current; + current = current->next(); + } + + if (!current) + return {}; + + Text splitText = Text(current, m_last); + + // Reset this Text's first and last atom pointers based on + // whether all or part of the content was extracted. + m_first = previous ? m_first : nullptr; + if (m_last = previous; m_last) + m_last->setNext(nullptr); + + return splitText; +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/text.h b/src/qdoc/qdoc/src/qdoc/text.h new file mode 100644 index 000000000..d42143909 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/text.h @@ -0,0 +1,87 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef TEXT_H +#define TEXT_H + +#include "atom.h" + +QT_BEGIN_NAMESPACE + +class Text +{ +public: + Text(); + explicit Text(const QString &str); + Text(const Text &text); + ~Text(); + + Text &operator=(const Text &text); + + Atom *firstAtom() { return m_first; } + Atom *lastAtom() { return m_last; } + Text &operator<<(Atom::AtomType atomType); + Text &operator<<(const QString &string); + Text &operator<<(const Atom &atom); + Text &operator<<(const LinkAtom &atom); + Text &operator<<(const Text &text); + void stripFirstAtom(); + void stripLastAtom(); + + [[nodiscard]] bool isEmpty() const { return m_first == nullptr; } + [[nodiscard]] bool contains(const QString &str) const; + [[nodiscard]] QString toString() const; + [[nodiscard]] const Atom *firstAtom() const { return m_first; } + [[nodiscard]] const Atom *lastAtom() const { return m_last; } + Text subText(Atom::AtomType left, Atom::AtomType right, const Atom *from = nullptr, + bool inclusive = false) const; + void dump() const; + void clear(); + + static Text subText(const Atom *begin, const Atom *end = nullptr); + static Text sectionHeading(const Atom *sectionBegin); + static int compare(const Text &text1, const Text &text2); + + [[nodiscard]] Text splitAtFirst(Atom::AtomType start); + +private: + Text(Atom *start, Atom *end) : m_first(start), m_last(end) + { + Q_ASSERT(m_first != nullptr); + Q_ASSERT(m_last != nullptr); + + m_last->setNext(nullptr); + } + + Atom *m_first { nullptr }; + Atom *m_last { nullptr }; +}; + +inline bool operator==(const Text &text1, const Text &text2) +{ + return Text::compare(text1, text2) == 0; +} +inline bool operator!=(const Text &text1, const Text &text2) +{ + return Text::compare(text1, text2) != 0; +} +inline bool operator<(const Text &text1, const Text &text2) +{ + return Text::compare(text1, text2) < 0; +} +inline bool operator<=(const Text &text1, const Text &text2) +{ + return Text::compare(text1, text2) <= 0; +} +inline bool operator>(const Text &text1, const Text &text2) +{ + return Text::compare(text1, text2) > 0; +} +inline bool operator>=(const Text &text1, const Text &text2) +{ + return Text::compare(text1, text2) >= 0; +} + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/tokenizer.cpp b/src/qdoc/qdoc/src/qdoc/tokenizer.cpp new file mode 100644 index 000000000..bfbfc53c5 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/tokenizer.cpp @@ -0,0 +1,788 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "tokenizer.h" + +#include "config.h" +#include "generator.h" + +#include <QtCore/qfile.h> +#include <QtCore/qhash.h> +#include <QtCore/qregularexpression.h> +#include <QtCore/qstring.h> +#include <QtCore/qstringconverter.h> + +#include <cctype> +#include <cstring> +#include <utility> + +QT_BEGIN_NAMESPACE + +#define LANGUAGE_CPP "Cpp" + +/* qmake ignore Q_OBJECT */ + +/* + Keep in sync with tokenizer.h. +*/ +static const char *kwords[] = { "char", + "class", + "const", + "double", + "enum", + "explicit", + "friend", + "inline", + "int", + "long", + "namespace", + "operator", + "private", + "protected", + "public", + "short", + "signals", + "signed", + "slots", + "static", + "struct", + "template", + "typedef", + "typename", + "union", + "unsigned", + "using", + "virtual", + "void", + "volatile", + "__int64", + "default", + "delete", + "final", + "override", + "Q_OBJECT", + "Q_OVERRIDE", + "Q_PROPERTY", + "Q_PRIVATE_PROPERTY", + "Q_DECLARE_SEQUENTIAL_ITERATOR", + "Q_DECLARE_MUTABLE_SEQUENTIAL_ITERATOR", + "Q_DECLARE_ASSOCIATIVE_ITERATOR", + "Q_DECLARE_MUTABLE_ASSOCIATIVE_ITERATOR", + "Q_DECLARE_FLAGS", + "Q_SIGNALS", + "Q_SLOTS", + "QT_COMPAT", + "QT_COMPAT_CONSTRUCTOR", + "QT_DEPRECATED", + "QT_MOC_COMPAT", + "QT_MODULE", + "QT3_SUPPORT", + "QT3_SUPPORT_CONSTRUCTOR", + "QT3_MOC_SUPPORT", + "QDOC_PROPERTY", + "QPrivateSignal" }; + +static const int KwordHashTableSize = 4096; +static int kwordHashTable[KwordHashTableSize]; + +static QHash<QByteArray, bool> *ignoredTokensAndDirectives = nullptr; + +static QRegularExpression *comment = nullptr; +static QRegularExpression *versionX = nullptr; +static QRegularExpression *definedX = nullptr; + +static QRegularExpression *defines = nullptr; +static QRegularExpression *falsehoods = nullptr; + +static QStringDecoder sourceDecoder; + +/* + This function is a perfect hash function for the 37 keywords of C99 + (with a hash table size of 512). It should perform well on our + Qt-enhanced C++ subset. +*/ +static int hashKword(const char *s, int len) +{ + return (((uchar)s[0]) + (((uchar)s[2]) << 5) + (((uchar)s[len - 1]) << 3)) % KwordHashTableSize; +} + +static void insertKwordIntoHash(const char *s, int number) +{ + int k = hashKword(s, int(strlen(s))); + while (kwordHashTable[k]) { + if (++k == KwordHashTableSize) + k = 0; + } + kwordHashTable[k] = number; +} + +Tokenizer::Tokenizer(const Location &loc, QFile &in) +{ + init(); + m_in = in.readAll(); + m_pos = 0; + start(loc); +} + +Tokenizer::Tokenizer(const Location &loc, QByteArray in) : m_in(std::move(in)) +{ + init(); + m_pos = 0; + start(loc); +} + +Tokenizer::~Tokenizer() +{ + delete[] m_lexBuf1; + delete[] m_lexBuf2; +} + +int Tokenizer::getToken() +{ + token_too_long_warning_was_issued = false; + + char *t = m_prevLex; + m_prevLex = m_lex; + m_lex = t; + + while (m_ch != EOF) { + m_tokLoc = m_curLoc; + m_lexLen = 0; + + if (isspace(m_ch)) { + do { + m_ch = getChar(); + } while (isspace(m_ch)); + } else if (isalpha(m_ch) || m_ch == '_') { + do { + m_ch = getChar(); + } while (isalnum(m_ch) || m_ch == '_'); + + int k = hashKword(m_lex, int(m_lexLen)); + for (;;) { + int i = kwordHashTable[k]; + if (i == 0) { + return Tok_Ident; + } else if (i == -1) { + if (!m_parsingMacro && ignoredTokensAndDirectives->contains(m_lex)) { + if (ignoredTokensAndDirectives->value(m_lex)) { // it's a directive + int parenDepth = 0; + while (m_ch != EOF && (m_ch != ')' || parenDepth > 1)) { + if (m_ch == '(') + ++parenDepth; + else if (m_ch == ')') + --parenDepth; + m_ch = getChar(); + } + if (m_ch == ')') + m_ch = getChar(); + } + break; + } + } else if (strcmp(m_lex, kwords[i - 1]) == 0) { + int ret = (int)Tok_FirstKeyword + i - 1; + if (ret != Tok_typename) + return ret; + break; + } + + if (++k == KwordHashTableSize) + k = 0; + } + } else if (isdigit(m_ch)) { + do { + m_ch = getChar(); + } while (isalnum(m_ch) || m_ch == '.' || m_ch == '+' || m_ch == '-'); + return Tok_Number; + } else { + switch (m_ch) { + case '!': + case '%': + m_ch = getChar(); + if (m_ch == '=') + m_ch = getChar(); + return Tok_SomeOperator; + case '"': + m_ch = getChar(); + + while (m_ch != EOF && m_ch != '"') { + if (m_ch == '\\') + m_ch = getChar(); + m_ch = getChar(); + } + m_ch = getChar(); + + if (m_ch == EOF) + m_tokLoc.warning( + QStringLiteral("Unterminated C++ string literal"), + QStringLiteral("Maybe you forgot '/*!' at the beginning of the file?")); + else + return Tok_String; + break; + case '#': + return getTokenAfterPreprocessor(); + case '&': + m_ch = getChar(); + /* + Removed check for '&&', only interpret '&=' as an operator. + '&&' is also used for an rvalue reference. QTBUG-32675 + */ + if (m_ch == '=') { + m_ch = getChar(); + return Tok_SomeOperator; + } else { + return Tok_Ampersand; + } + case '\'': + m_ch = getChar(); + /* + Allow empty character literal. QTBUG-25775 + */ + if (m_ch == '\'') { + m_ch = getChar(); + break; + } + if (m_ch == '\\') + m_ch = getChar(); + do { + m_ch = getChar(); + } while (m_ch != EOF && m_ch != '\''); + + if (m_ch == EOF) { + m_tokLoc.warning(QStringLiteral("Unterminated C++ character literal")); + } else { + m_ch = getChar(); + return Tok_Number; + } + break; + case '(': + m_ch = getChar(); + if (m_numPreprocessorSkipping == 0) + m_parenDepth++; + if (isspace(m_ch)) { + do { + m_ch = getChar(); + } while (isspace(m_ch)); + m_lexLen = 1; + m_lex[1] = '\0'; + } + if (m_ch == '*') { + m_ch = getChar(); + return Tok_LeftParenAster; + } + return Tok_LeftParen; + case ')': + m_ch = getChar(); + if (m_numPreprocessorSkipping == 0) + m_parenDepth--; + return Tok_RightParen; + case '*': + m_ch = getChar(); + if (m_ch == '=') { + m_ch = getChar(); + return Tok_SomeOperator; + } else { + return Tok_Aster; + } + case '^': + m_ch = getChar(); + if (m_ch == '=') { + m_ch = getChar(); + return Tok_SomeOperator; + } else { + return Tok_Caret; + } + case '+': + m_ch = getChar(); + if (m_ch == '+' || m_ch == '=') + m_ch = getChar(); + return Tok_SomeOperator; + case ',': + m_ch = getChar(); + return Tok_Comma; + case '-': + m_ch = getChar(); + if (m_ch == '-' || m_ch == '=') { + m_ch = getChar(); + } else if (m_ch == '>') { + m_ch = getChar(); + if (m_ch == '*') + m_ch = getChar(); + } + return Tok_SomeOperator; + case '.': + m_ch = getChar(); + if (m_ch == '*') { + m_ch = getChar(); + } else if (m_ch == '.') { + do { + m_ch = getChar(); + } while (m_ch == '.'); + return Tok_Ellipsis; + } else if (isdigit(m_ch)) { + do { + m_ch = getChar(); + } while (isalnum(m_ch) || m_ch == '.' || m_ch == '+' || m_ch == '-'); + return Tok_Number; + } + return Tok_SomeOperator; + case '/': + m_ch = getChar(); + if (m_ch == '/') { + do { + m_ch = getChar(); + } while (m_ch != EOF && m_ch != '\n'); + } else if (m_ch == '*') { + bool metDoc = false; // empty doc is no doc + bool metSlashAsterBang = false; + bool metAster = false; + bool metAsterSlash = false; + + m_ch = getChar(); + if (m_ch == '!') + metSlashAsterBang = true; + + while (!metAsterSlash) { + if (m_ch == EOF) { + m_tokLoc.warning(QStringLiteral("Unterminated C++ comment")); + break; + } else { + if (m_ch == '*') { + metAster = true; + } else if (metAster && m_ch == '/') { + metAsterSlash = true; + } else { + metAster = false; + if (isgraph(m_ch)) + metDoc = true; + } + } + m_ch = getChar(); + } + if (metSlashAsterBang && metDoc) + return Tok_Doc; + else if (m_parenDepth > 0) + return Tok_Comment; + } else { + if (m_ch == '=') + m_ch = getChar(); + return Tok_SomeOperator; + } + break; + case ':': + m_ch = getChar(); + if (m_ch == ':') { + m_ch = getChar(); + return Tok_Gulbrandsen; + } else { + return Tok_Colon; + } + case ';': + m_ch = getChar(); + return Tok_Semicolon; + case '<': + m_ch = getChar(); + if (m_ch == '<') { + m_ch = getChar(); + if (m_ch == '=') + m_ch = getChar(); + return Tok_SomeOperator; + } else if (m_ch == '=') { + m_ch = getChar(); + return Tok_SomeOperator; + } else { + return Tok_LeftAngle; + } + case '=': + m_ch = getChar(); + if (m_ch == '=') { + m_ch = getChar(); + return Tok_SomeOperator; + } else { + return Tok_Equal; + } + case '>': + m_ch = getChar(); + if (m_ch == '>') { + m_ch = getChar(); + if (m_ch == '=') + m_ch = getChar(); + return Tok_SomeOperator; + } else if (m_ch == '=') { + m_ch = getChar(); + return Tok_SomeOperator; + } else { + return Tok_RightAngle; + } + case '?': + m_ch = getChar(); + return Tok_SomeOperator; + case '[': + m_ch = getChar(); + if (m_numPreprocessorSkipping == 0) + m_bracketDepth++; + return Tok_LeftBracket; + case '\\': + m_ch = getChar(); + m_ch = getChar(); // skip one character + break; + case ']': + m_ch = getChar(); + if (m_numPreprocessorSkipping == 0) + m_bracketDepth--; + return Tok_RightBracket; + case '{': + m_ch = getChar(); + if (m_numPreprocessorSkipping == 0) + m_braceDepth++; + return Tok_LeftBrace; + case '}': + m_ch = getChar(); + if (m_numPreprocessorSkipping == 0) + m_braceDepth--; + return Tok_RightBrace; + case '|': + m_ch = getChar(); + if (m_ch == '|' || m_ch == '=') + m_ch = getChar(); + return Tok_SomeOperator; + case '~': + m_ch = getChar(); + return Tok_Tilde; + case '@': + m_ch = getChar(); + return Tok_At; + default: + // ### We should really prevent qdoc from looking at snippet files rather than + // ### suppress warnings when reading them. + if (m_numPreprocessorSkipping == 0 + && !(m_tokLoc.fileName().endsWith(".qdoc") + || m_tokLoc.fileName().endsWith(".js"))) { + m_tokLoc.warning(QStringLiteral("Hostile character 0x%1 in C++ source") + .arg((uchar)m_ch, 1, 16)); + } + m_ch = getChar(); + } + } + } + + if (m_preprocessorSkipping.size() > 1) { + m_tokLoc.warning(QStringLiteral("Expected #endif before end of file")); + // clear it out or we get an infinite loop! + while (!m_preprocessorSkipping.isEmpty()) { + popSkipping(); + } + } + + strcpy(m_lex, "end-of-input"); + m_lexLen = strlen(m_lex); + return Tok_Eoi; +} + +void Tokenizer::initialize() +{ + Config &config = Config::instance(); + QString versionSym = config.get(CONFIG_VERSIONSYM).asString(); + const QLatin1String defaultEncoding("UTF-8"); + + QString sourceEncoding = config.get(CONFIG_SOURCEENCODING).asString(defaultEncoding); + if (!QStringConverter::encodingForName(sourceEncoding.toUtf8().constData())) { + Location().warning(QStringLiteral("Source encoding '%1' not supported, using '%2' as default.") + .arg(sourceEncoding, defaultEncoding)); + sourceEncoding = defaultEncoding; + } + sourceDecoder = QStringDecoder(sourceEncoding.toUtf8().constData()); + Q_ASSERT(sourceDecoder.isValid()); + + comment = new QRegularExpression("/(?:\\*.*\\*/|/.*\n|/[^\n]*$)", QRegularExpression::InvertedGreedinessOption); + versionX = new QRegularExpression("$cannot possibly match^"); + if (!versionSym.isEmpty()) + versionX->setPattern("^[ \t]*(?:" + QRegularExpression::escape(versionSym) + + ")[ \t]+\"([^\"]*)\"[ \t]*$"); + definedX = new QRegularExpression("^defined ?\\(?([A-Z_0-9a-z]+) ?\\)?$"); + + QStringList d{config.get(CONFIG_DEFINES).asStringList()}; + d += "qdoc"; + defines = new QRegularExpression(QRegularExpression::anchoredPattern(d.join('|'))); + falsehoods = new QRegularExpression(QRegularExpression::anchoredPattern( + config.get(CONFIG_FALSEHOODS).asStringList().join('|'))); + + /* + The keyword hash table is always cleared before any words are inserted. + */ + memset(kwordHashTable, 0, sizeof(kwordHashTable)); + for (int i = 0; i < Tok_LastKeyword - Tok_FirstKeyword + 1; i++) + insertKwordIntoHash(kwords[i], i + 1); + + ignoredTokensAndDirectives = new QHash<QByteArray, bool>; + + const QStringList tokens{config.get(LANGUAGE_CPP + + Config::dot + + CONFIG_IGNORETOKENS).asStringList()}; + for (const auto &token : tokens) { + const QByteArray tb = token.toLatin1(); + ignoredTokensAndDirectives->insert(tb, false); + insertKwordIntoHash(tb.data(), -1); + } + + const QStringList directives{config.get(LANGUAGE_CPP + + Config::dot + + CONFIG_IGNOREDIRECTIVES).asStringList()}; + for (const auto &directive : directives) { + const QByteArray db = directive.toLatin1(); + ignoredTokensAndDirectives->insert(db, true); + insertKwordIntoHash(db.data(), -1); + } +} + +/*! + The heap allocated variables are freed here. The keyword + hash table is not cleared here, but it is cleared in the + initialize() function, before any keywords are inserted. + */ +void Tokenizer::terminate() +{ + delete comment; + comment = nullptr; + delete versionX; + versionX = nullptr; + delete definedX; + definedX = nullptr; + delete defines; + defines = nullptr; + delete falsehoods; + falsehoods = nullptr; + delete ignoredTokensAndDirectives; + ignoredTokensAndDirectives = nullptr; +} + +void Tokenizer::init() +{ + m_lexBuf1 = new char[(int)yyLexBufSize]; + m_lexBuf2 = new char[(int)yyLexBufSize]; + m_prevLex = m_lexBuf1; + m_prevLex[0] = '\0'; + m_lex = m_lexBuf2; + m_lex[0] = '\0'; + m_lexLen = 0; + m_preprocessorSkipping.push(false); + m_numPreprocessorSkipping = 0; + m_braceDepth = 0; + m_parenDepth = 0; + m_bracketDepth = 0; + m_ch = '\0'; + m_parsingMacro = false; +} + +void Tokenizer::start(const Location &loc) +{ + m_tokLoc = loc; + m_curLoc = loc; + m_curLoc.start(); + strcpy(m_prevLex, "beginning-of-input"); + strcpy(m_lex, "beginning-of-input"); + m_lexLen = strlen(m_lex); + m_braceDepth = 0; + m_parenDepth = 0; + m_bracketDepth = 0; + m_ch = '\0'; + m_ch = getChar(); +} + +/* + Returns the next token, if # was met. This function interprets the + preprocessor directive, skips over any #ifdef'd out tokens, and returns the + token after all of that. +*/ +int Tokenizer::getTokenAfterPreprocessor() +{ + m_ch = getChar(); + while (isspace(m_ch) && m_ch != '\n') + m_ch = getChar(); + + /* + #directive condition + */ + QString directive; + QString condition; + + while (isalpha(m_ch)) { + directive += QChar(m_ch); + m_ch = getChar(); + } + if (!directive.isEmpty()) { + while (m_ch != EOF && m_ch != '\n') { + if (m_ch == '\\') { + m_ch = getChar(); + if (m_ch == '\r') + m_ch = getChar(); + } + condition += QChar(m_ch); + m_ch = getChar(); + } + condition.remove(*comment); + condition = condition.simplified(); + + /* + The #if, #ifdef, #ifndef, #elif, #else, and #endif + directives have an effect on the skipping stack. For + instance, if the code processed so far is + + #if 1 + #if 0 + #if 1 + // ... + #else + + the skipping stack contains, from bottom to top, false true + true (assuming 0 is false and 1 is true). If at least one + entry of the stack is true, the tokens are skipped. + + This mechanism is simple yet hard to understand. + */ + if (directive[0] == QChar('i')) { + if (directive == QString("if")) + pushSkipping(!isTrue(condition)); + else if (directive == QString("ifdef")) + pushSkipping(!defines->match(condition).hasMatch()); + else if (directive == QString("ifndef")) + pushSkipping(defines->match(condition).hasMatch()); + } else if (directive[0] == QChar('e')) { + if (directive == QString("elif")) { + bool old = popSkipping(); + if (old) + pushSkipping(!isTrue(condition)); + else + pushSkipping(true); + } else if (directive == QString("else")) { + pushSkipping(!popSkipping()); + } else if (directive == QString("endif")) { + popSkipping(); + } + } else if (directive == QString("define")) { + auto match = versionX->match(condition); + if (match.hasMatch()) + m_version = match.captured(1); + } + } + + int tok; + do { + /* + We set yyLex now, and after getToken() this will be + yyPrevLex. This way, we skip over the preprocessor + directive. + */ + qstrcpy(m_lex, m_prevLex); + + /* + If getToken() meets another #, it will call + getTokenAfterPreprocessor() once again, which could in turn + call getToken() again, etc. Unless there are 10,000 or so + preprocessor directives in a row, this shouldn't overflow + the stack. + */ + tok = getToken(); + } while (m_numPreprocessorSkipping > 0 && tok != Tok_Eoi); + return tok; +} + +/* + Pushes a new skipping value onto the stack. This corresponds to entering a + new #if block. +*/ +void Tokenizer::pushSkipping(bool skip) +{ + m_preprocessorSkipping.push(skip); + if (skip) + m_numPreprocessorSkipping++; +} + +/* + Pops a skipping value from the stack. This corresponds to reaching a #endif. +*/ +bool Tokenizer::popSkipping() +{ + if (m_preprocessorSkipping.isEmpty()) { + m_tokLoc.warning(QStringLiteral("Unexpected #elif, #else or #endif")); + return true; + } + + bool skip = m_preprocessorSkipping.pop(); + if (skip) + m_numPreprocessorSkipping--; + return skip; +} + +/* + Returns \c true if the condition evaluates as true, otherwise false. The + condition is represented by a string. Unsophisticated parsing techniques are + used. The preprocessing method could be named StriNg-Oriented PreProcessing, + as SNOBOL stands for StriNg-Oriented symBOlic Language. +*/ +bool Tokenizer::isTrue(const QString &condition) +{ + int firstOr = -1; + int firstAnd = -1; + int parenDepth = 0; + + /* + Find the first logical operator at top level, but be careful + about precedence. Examples: + + X || Y // the or + X || Y || Z // the leftmost or + X || Y && Z // the or + X && Y || Z // the or + (X || Y) && Z // the and + */ + for (int i = 0; i < condition.size() - 1; i++) { + QChar ch = condition[i]; + if (ch == QChar('(')) { + parenDepth++; + } else if (ch == QChar(')')) { + parenDepth--; + } else if (parenDepth == 0) { + if (condition[i + 1] == ch) { + if (ch == QChar('|')) { + firstOr = i; + break; + } else if (ch == QChar('&')) { + if (firstAnd == -1) + firstAnd = i; + } + } + } + } + if (firstOr != -1) + return isTrue(condition.left(firstOr)) || isTrue(condition.mid(firstOr + 2)); + if (firstAnd != -1) + return isTrue(condition.left(firstAnd)) && isTrue(condition.mid(firstAnd + 2)); + + QString t = condition.simplified(); + if (t.isEmpty()) + return true; + + if (t[0] == QChar('!')) + return !isTrue(t.mid(1)); + if (t[0] == QChar('(') && t.endsWith(QChar(')'))) + return isTrue(t.mid(1, t.size() - 2)); + + auto match = definedX->match(t); + if (match.hasMatch()) + return defines->match(match.captured(1)).hasMatch(); + else + return !falsehoods->match(t).hasMatch(); +} + +QString Tokenizer::lexeme() const +{ + return sourceDecoder(m_lex); +} + +QString Tokenizer::previousLexeme() const +{ + return sourceDecoder(m_prevLex); +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/tokenizer.h b/src/qdoc/qdoc/src/qdoc/tokenizer.h new file mode 100644 index 000000000..d5669dfb7 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/tokenizer.h @@ -0,0 +1,179 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef TOKENIZER_H +#define TOKENIZER_H + +#include "location.h" + +#include <QtCore/qfile.h> +#include <QtCore/qstack.h> +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +/* + Here come the C++ tokens we support. The first part contains + all-purpose tokens; then come keywords. + + If you add a keyword, make sure to modify the keyword array in + tokenizer.cpp as well, and possibly adjust Tok_FirstKeyword and + Tok_LastKeyword. +*/ +enum { + Tok_Eoi, + Tok_Ampersand, + Tok_Aster, + Tok_Caret, + Tok_LeftParen, + Tok_RightParen, + Tok_LeftParenAster, + Tok_Equal, + Tok_LeftBrace, + Tok_RightBrace, + Tok_Semicolon, + Tok_Colon, + Tok_LeftAngle, + Tok_RightAngle, + Tok_Comma, + Tok_Ellipsis, + Tok_Gulbrandsen, + Tok_LeftBracket, + Tok_RightBracket, + Tok_Tilde, + Tok_SomeOperator, + Tok_Number, + Tok_String, + Tok_Doc, + Tok_Comment, + Tok_Ident, + Tok_At, + Tok_char, + Tok_class, + Tok_const, + Tok_double, + Tok_int, + Tok_long, + Tok_operator, + Tok_short, + Tok_signed, + Tok_typename, + Tok_unsigned, + Tok_void, + Tok_volatile, + Tok_int64, + Tok_QPrivateSignal, + Tok_FirstKeyword = Tok_char, + Tok_LastKeyword = Tok_QPrivateSignal +}; + +/* + The Tokenizer class implements lexical analysis of C++ source + files. + + Not every operator or keyword of C++ is recognized; only those + that are interesting to us. Some Qt keywords or macros are also + recognized. +*/ + +class Tokenizer +{ +public: + Tokenizer(const Location &loc, QByteArray in); + Tokenizer(const Location &loc, QFile &file); + + ~Tokenizer(); + + int getToken(); + void setParsingFnOrMacro(bool macro) { m_parsingMacro = macro; } + + [[nodiscard]] const Location &location() const { return m_tokLoc; } + [[nodiscard]] QString previousLexeme() const; + [[nodiscard]] QString lexeme() const; + [[nodiscard]] QString version() const { return m_version; } + [[nodiscard]] int parenDepth() const { return m_parenDepth; } + [[nodiscard]] int bracketDepth() const { return m_bracketDepth; } + + static void initialize(); + static void terminate(); + static bool isTrue(const QString &condition); + +private: + void init(); + void start(const Location &loc); + /* + Represents the maximum amount of characters that a token can be composed + of. + + When a token with more characters than the maximum amount is encountered, a + warning is issued and parsing continues, discarding all characters from the + currently parsed token that don't fit into the buffer. + */ + enum { yyLexBufSize = 1048576 }; + + int getch() { return m_pos == m_in.size() ? EOF : m_in[m_pos++]; } + + inline int getChar() + { + using namespace Qt::StringLiterals; + + if (m_ch == EOF) + return EOF; + if (m_lexLen < yyLexBufSize - 1) { + m_lex[m_lexLen++] = (char)m_ch; + m_lex[m_lexLen] = '\0'; + } else if (!token_too_long_warning_was_issued) { + location().warning( + u"The content is too long.\n"_s, + u"The maximum amount of characters for this content is %1.\n"_s.arg(yyLexBufSize) + + "Consider splitting it or reducing its size." + ); + + token_too_long_warning_was_issued = true; + } + m_curLoc.advance(QChar(m_ch)); + int ch = getch(); + if (ch == EOF) + return EOF; + // cast explicitly to make sure the value of ch + // is in range [0..255] to avoid assert messages + // when using debug CRT that checks its input. + return int(uint(uchar(ch))); + } + + int getTokenAfterPreprocessor(); + void pushSkipping(bool skip); + bool popSkipping(); + + Location m_tokLoc; + Location m_curLoc; + char *m_lexBuf1 { nullptr }; + char *m_lexBuf2 { nullptr }; + char *m_prevLex { nullptr }; + char *m_lex { nullptr }; + size_t m_lexLen {}; + QStack<bool> m_preprocessorSkipping; + int m_numPreprocessorSkipping {}; + int m_braceDepth {}; + int m_parenDepth {}; + int m_bracketDepth {}; + int m_ch {}; + + QString m_version {}; + bool m_parsingMacro {}; + + // Used to ensure that the warning that is issued when a token is + // too long to fit into our fixed sized buffer is not repeated for each + // character of that token after the last saved one. + // The flag is reset whenever a new token is requested, so as to allow + // reporting all such tokens that are too long during a single execution. + bool token_too_long_warning_was_issued{false}; + +protected: + QByteArray m_in {}; + int m_pos {}; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/topic.h b/src/qdoc/qdoc/src/qdoc/topic.h new file mode 100644 index 000000000..1f9646864 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/topic.h @@ -0,0 +1,29 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +#ifndef TOPIC_H +#define TOPIC_H + +QT_BEGIN_NAMESPACE + +struct Topic +{ +public: + Topic() = default; + Topic(QString &t, QString a) : m_topic(t), m_args(std::move(a)) { } + ~Topic() = default; + + [[nodiscard]] bool isEmpty() const { return m_topic.isEmpty(); } + void clear() + { + m_topic.clear(); + m_args.clear(); + } + + QString m_topic {}; + QString m_args {}; +}; +typedef QList<Topic> TopicList; + +QT_END_NAMESPACE + +#endif // TOPIC_H diff --git a/src/qdoc/qdoc/src/qdoc/tree.cpp b/src/qdoc/qdoc/src/qdoc/tree.cpp new file mode 100644 index 000000000..5c8f7d6d1 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/tree.cpp @@ -0,0 +1,1357 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "tree.h" + +#include "classnode.h" +#include "collectionnode.h" +#include "doc.h" +#include "enumnode.h" +#include "functionnode.h" +#include "htmlgenerator.h" +#include "location.h" +#include "node.h" +#include "qdocdatabase.h" +#include "text.h" +#include "typedefnode.h" + +QT_BEGIN_NAMESPACE + +/*! + \class Tree + + This class constructs and maintains a tree of instances of + the subclasses of Node. + + This class is now private. Only class QDocDatabase has access. + Please don't change this. If you must access class Tree, do it + though the pointer to the singleton QDocDatabase. + + Tree is being converted to a forest. A static member provides a + map of Tree *values with the module names as the keys. There is + one Tree in the map for each index file read, and there is one + tree that is not in the map for the module whose documentation + is being generated. + */ + +/*! + Constructs a Tree. \a qdb is the pointer to the singleton + qdoc database that is constructing the tree. This might not + be necessary, and it might be removed later. + + \a camelCaseModuleName is the project name for this tree + as it appears in the qdocconf file. + */ +Tree::Tree(const QString &camelCaseModuleName, QDocDatabase *qdb) + : m_camelCaseModuleName(camelCaseModuleName), + m_physicalModuleName(camelCaseModuleName.toLower()), + m_qdb(qdb), + m_root(nullptr, QString()) +{ + m_root.setPhysicalModuleName(m_physicalModuleName); + m_root.setTree(this); +} + +/*! + Destroys the Tree. + + There are two maps of targets, keywords, and contents. + One map is indexed by ref, the other by title. Both maps + use the same set of TargetRec objects as the values, + so we only need to delete the values from one of them. + + The Node instances themselves are destroyed by the root + node's (\c m_root) destructor. + */ +Tree::~Tree() +{ + qDeleteAll(m_nodesByTargetRef); + m_nodesByTargetRef.clear(); + m_nodesByTargetTitle.clear(); +} + +/* API members */ + +/*! + Calls findClassNode() first with \a path and \a start. If + it finds a node, the node is returned. If not, it calls + findNamespaceNode() with the same parameters. The result + is returned. + */ +Node *Tree::findNodeForInclude(const QStringList &path) const +{ + Node *n = findClassNode(path); + if (n == nullptr) + n = findNamespaceNode(path); + return n; +} + +/*! + This function searches this tree for an Aggregate node with + the specified \a name. It returns the pointer to that node + or nullptr. + + We might need to split the name on '::' but we assume the + name is a single word at the moment. + */ +Aggregate *Tree::findAggregate(const QString &name) +{ + QStringList path = name.split(QLatin1String("::")); + return static_cast<Aggregate *>(findNodeRecursive(path, 0, const_cast<NamespaceNode *>(root()), + &Node::isFirstClassAggregate)); +} + +/*! + Find the C++ class node named \a path. Begin the search at the + \a start node. If the \a start node is 0, begin the search + at the root of the tree. Only a C++ class node named \a path is + acceptible. If one is not found, 0 is returned. + */ +ClassNode *Tree::findClassNode(const QStringList &path, const Node *start) const +{ + if (start == nullptr) + start = const_cast<NamespaceNode *>(root()); + return static_cast<ClassNode *>(findNodeRecursive(path, 0, start, &Node::isClassNode)); +} + +/*! + Find the Namespace node named \a path. Begin the search at + the root of the tree. Only a Namespace node named \a path + is acceptible. If one is not found, 0 is returned. + */ +NamespaceNode *Tree::findNamespaceNode(const QStringList &path) const +{ + Node *start = const_cast<NamespaceNode *>(root()); + return static_cast<NamespaceNode *>(findNodeRecursive(path, 0, start, &Node::isNamespace)); +} + +/*! + This function searches for the node specified by \a path. + The matching node can be one of several different types + including a C++ class, a C++ namespace, or a C++ header + file. + + I'm not sure if it can be a QML type, but if that is a + possibility, the code can easily accommodate it. + + If a matching node is found, a pointer to it is returned. + Otherwise 0 is returned. + */ +Aggregate *Tree::findRelatesNode(const QStringList &path) +{ + Node *n = findNodeRecursive(path, 0, root(), &Node::isRelatableType); + return (((n != nullptr) && n->isAggregate()) ? static_cast<Aggregate *>(n) : nullptr); +} + +/*! + Inserts function name \a funcName and function role \a funcRole into + the property function map for the specified \a property. + */ +void Tree::addPropertyFunction(PropertyNode *property, const QString &funcName, + PropertyNode::FunctionRole funcRole) +{ + m_unresolvedPropertyMap[property].insert(funcRole, funcName); +} + +/*! + This function resolves C++ inheritance and reimplementation + settings for each C++ class node found in the tree beginning + at \a n. It also calls itself recursively for each C++ class + node or namespace node it encounters. + + This function does not resolve QML inheritance. + */ +void Tree::resolveBaseClasses(Aggregate *n) +{ + for (auto it = n->constBegin(); it != n->constEnd(); ++it) { + if ((*it)->isClassNode()) { + auto *cn = static_cast<ClassNode *>(*it); + QList<RelatedClass> &bases = cn->baseClasses(); + for (auto &base : bases) { + if (base.m_node == nullptr) { + Node *n = m_qdb->findClassNode(base.m_path); + /* + If the node for the base class was not found, + the reason might be that the subclass is in a + namespace and the base class is in the same + namespace, but the base class name was not + qualified with the namespace name. That is the + case most of the time. Then restart the search + at the parent of the subclass node (the namespace + node) using the unqualified base class name. + */ + if (n == nullptr) { + Aggregate *parent = cn->parent(); + if (parent != nullptr) + // Exclude the root namespace + if (parent->isNamespace() && !parent->name().isEmpty()) + n = findClassNode(base.m_path, parent); + } + if (n != nullptr) { + auto *bcn = static_cast<ClassNode *>(n); + base.m_node = bcn; + bcn->addDerivedClass(base.m_access, cn); + } + } + } + resolveBaseClasses(cn); + } else if ((*it)->isNamespace()) { + resolveBaseClasses(static_cast<NamespaceNode *>(*it)); + } + } +} + +/*! + */ +void Tree::resolvePropertyOverriddenFromPtrs(Aggregate *n) +{ + for (auto node = n->constBegin(); node != n->constEnd(); ++node) { + if ((*node)->isClassNode()) { + auto *cn = static_cast<ClassNode *>(*node); + for (auto property = cn->constBegin(); property != cn->constEnd(); ++property) { + if ((*property)->isProperty()) + cn->resolvePropertyOverriddenFromPtrs(static_cast<PropertyNode *>(*property)); + } + resolvePropertyOverriddenFromPtrs(cn); + } else if ((*node)->isNamespace()) { + resolvePropertyOverriddenFromPtrs(static_cast<NamespaceNode *>(*node)); + } + } +} + +/*! + Resolves access functions associated with each PropertyNode stored + in \c m_unresolvedPropertyMap, and adds them into the property node. + This allows the property node to list the access functions when + generating their documentation. + */ +void Tree::resolveProperties() +{ + for (auto propEntry = m_unresolvedPropertyMap.constBegin(); + propEntry != m_unresolvedPropertyMap.constEnd(); ++propEntry) { + PropertyNode *property = propEntry.key(); + Aggregate *parent = property->parent(); + QString getterName = (*propEntry)[PropertyNode::FunctionRole::Getter]; + QString setterName = (*propEntry)[PropertyNode::FunctionRole::Setter]; + QString resetterName = (*propEntry)[PropertyNode::FunctionRole::Resetter]; + QString notifierName = (*propEntry)[PropertyNode::FunctionRole::Notifier]; + QString bindableName = (*propEntry)[PropertyNode::FunctionRole::Bindable]; + + for (auto it = parent->constBegin(); it != parent->constEnd(); ++it) { + if ((*it)->isFunction()) { + auto *function = static_cast<FunctionNode *>(*it); + if (function->access() == property->access() + && (function->status() == property->status() || function->doc().isEmpty())) { + if (function->name() == getterName) { + property->addFunction(function, PropertyNode::FunctionRole::Getter); + } else if (function->name() == setterName) { + property->addFunction(function, PropertyNode::FunctionRole::Setter); + } else if (function->name() == resetterName) { + property->addFunction(function, PropertyNode::FunctionRole::Resetter); + } else if (function->name() == notifierName) { + property->addSignal(function, PropertyNode::FunctionRole::Notifier); + } else if (function->name() == bindableName) { + property->addFunction(function, PropertyNode::FunctionRole::Bindable); + } + } + } + } + } + + for (auto propEntry = m_unresolvedPropertyMap.constBegin(); + propEntry != m_unresolvedPropertyMap.constEnd(); ++propEntry) { + PropertyNode *property = propEntry.key(); + // redo it to set the property functions + if (property->overriddenFrom()) + property->setOverriddenFrom(property->overriddenFrom()); + } + + m_unresolvedPropertyMap.clear(); +} + +/*! + For each QML class node that points to a C++ class node, + follow its C++ class node pointer and set the C++ class + node's QML class node pointer back to the QML class node. + */ +void Tree::resolveCppToQmlLinks() +{ + + const NodeList &children = m_root.childNodes(); + for (auto *child : children) { + if (child->isQmlType()) { + auto *qcn = static_cast<QmlTypeNode *>(child); + auto *cn = const_cast<ClassNode *>(qcn->classNode()); + if (cn) + cn->insertQmlNativeType(qcn); + } + } +} + +/*! + For each \a aggregate, recursively set the \\since version based on + \\since information from the associated physical or logical module. + That is, C++ and QML types inherit the \\since of their module, + unless that command is explicitly used in the type documentation. + + In addition, resolve the since information for individual enum + values. +*/ +void Tree::resolveSince(Aggregate &aggregate) +{ + for (auto *child : aggregate.childNodes()) { + // Order matters; resolve since-clauses in enum values + // first as EnumNode is not an Aggregate + if (child->isEnumType()) + resolveEnumValueSince(static_cast<EnumNode&>(*child)); + if (!child->isAggregate()) + continue; + if (!child->since().isEmpty()) + continue; + + if (const auto collectionNode = m_qdb->getModuleNode(child)) + child->setSince(collectionNode->since()); + + resolveSince(static_cast<Aggregate&>(*child)); + } +} + +/*! + Resolve since information for values of enum node \a en. + + Enum values are not derived from Node, but they can have + 'since' information associated with them. Since-strings + for each enum item are initially stored in the Doc + instance of EnumNode as SinceTag atoms; parse the doc + and store them into each EnumItem. +*/ +void Tree::resolveEnumValueSince(EnumNode &en) +{ + const QStringList enumItems{en.doc().enumItemNames()}; + const Atom *atom = en.doc().body().firstAtom(); + while ((atom = atom->find(Atom::ListTagLeft))) { + if (atom = atom->next(); !atom) + break; + if (auto val = atom->string(); enumItems.contains(val)) { + if (atom = atom->next(); atom && atom->next(Atom::SinceTagLeft)) + en.setSince(val, atom->next()->next()->string()); + } + } +} + +/*! + Traverse this Tree and for each ClassNode found, remove + from its list of base classes any that are marked private + or internal. When a class is removed from a base class + list, promote its public pase classes to be base classes + of the class where the base class was removed. This is + done for documentation purposes. The function is recursive + on namespace nodes. + */ +void Tree::removePrivateAndInternalBases(NamespaceNode *rootNode) +{ + if (rootNode == nullptr) + rootNode = root(); + + for (auto node = rootNode->constBegin(); node != rootNode->constEnd(); ++node) { + if ((*node)->isClassNode()) + static_cast<ClassNode *>(*node)->removePrivateAndInternalBases(); + else if ((*node)->isNamespace()) + removePrivateAndInternalBases(static_cast<NamespaceNode *>(*node)); + } +} + +/*! + */ +ClassList Tree::allBaseClasses(const ClassNode *classNode) const +{ + ClassList result; + const auto &baseClasses = classNode->baseClasses(); + for (const auto &relatedClass : baseClasses) { + if (relatedClass.m_node != nullptr) { + result += relatedClass.m_node; + result += allBaseClasses(relatedClass.m_node); + } + } + return result; +} + +/*! + Find the node with the specified \a path name that is of + the specified \a type and \a subtype. Begin the search at + the \a start node. If the \a start node is 0, begin the + search at the tree root. \a subtype is not used unless + \a type is \c{Page}. + */ +Node *Tree::findNodeByNameAndType(const QStringList &path, bool (Node::*isMatch)() const) const +{ + return findNodeRecursive(path, 0, root(), isMatch); +} + +/*! + Recursive search for a node identified by \a path. Each + path element is a name. \a pathIndex specifies the index + of the name in \a path to try to match. \a start is the + node whose children shoulod be searched for one that has + that name. Each time a match is found, increment the + \a pathIndex and call this function recursively. + + If the end of the path is reached (i.e. if a matching + node is found for each name in the \a path), the \a type + must match the type of the last matching node, and if the + type is \e{Page}, the \a subtype must match as well. + + If the algorithm is successful, the pointer to the final + node is returned. Otherwise 0 is returned. + */ +Node *Tree::findNodeRecursive(const QStringList &path, int pathIndex, const Node *start, + bool (Node::*isMatch)() const) const +{ + if (start == nullptr || path.isEmpty()) + return nullptr; + Node *node = const_cast<Node *>(start); + if (!node->isAggregate()) + return ((pathIndex >= path.size()) ? node : nullptr); + auto *current = static_cast<Aggregate *>(node); + const NodeList &children = current->childNodes(); + const QString &name = path.at(pathIndex); + for (auto *node : children) { + if (node == nullptr) + continue; + if (node->name() == name) { + if (pathIndex + 1 >= path.size()) { + if ((node->*(isMatch))()) + return node; + continue; + } else { // Search the children of n for the next name in the path. + node = findNodeRecursive(path, pathIndex + 1, node, isMatch); + if (node != nullptr) + return node; + } + } + } + return nullptr; +} + +/*! + Searches the tree for a node that matches the \a path plus + the \a target. The search begins at \a start and moves up + the parent chain from there, or, if \a start is 0, the search + begins at the root. + + The \a flags can indicate whether to search base classes and/or + the enum values in enum types. \a genus further restricts + the type of nodes to match, i.e. CPP or QML. + + If a matching node is found, \a ref is set to the HTML fragment + identifier to use for the link. On return, the optional + \a targetType parameter contains the type of the resolved + target; section title (Contents), \\target, \\keyword, or other + (Unknown). + */ +const Node *Tree::findNodeForTarget(const QStringList &path, const QString &target, + const Node *start, int flags, Node::Genus genus, + QString &ref, TargetRec::TargetType *targetType) const +{ + const Node *node = nullptr; + + // Retrieves and sets ref from target for Node n. + // Returns n on valid (or empty) target, or nullptr on an invalid target. + auto set_ref_from_target = [this, &ref, &target](const Node *n) -> const Node* { + if (!target.isEmpty()) { + if (ref = getRef(target, n); ref.isEmpty()) + return nullptr; + } + return n; + }; + + if (genus == Node::DontCare || genus == Node::DOC) { + if (node = findPageNodeByTitle(path.at(0)); node) { + if (node = set_ref_from_target(node); node) + return node; + } + } + + const TargetRec *result = findUnambiguousTarget(path.join(QLatin1String("::")), genus); + if (result) { + ref = result->m_ref; + if (node = set_ref_from_target(result->m_node); node) { + // Delay returning references to section titles as we + // may find a better match below + if (result->m_type != TargetRec::Contents) { + if (targetType) + *targetType = result->m_type; + return node; + } + ref.clear(); + } + } + + const Node *current = start ? start : root(); + /* + If the path contains one or two double colons ("::"), + check if the first two path elements refer to a QML type. + If so, path[0] is QML module identifier, and path[1] is + the type. + */ + int path_idx = 0; + if ((genus == Node::QML || genus == Node::DontCare) + && path.size() >= 2 && !path[0].isEmpty()) { + if (auto *qcn = lookupQmlType(path.sliced(0, 2).join(QLatin1String("::"))); qcn) { + current = qcn; + // No further elements in the path, return the type + if (path.size() == 2) + return set_ref_from_target(qcn); + path_idx = 2; + } + } + + while (current) { + if (current->isAggregate()) { + if (const Node *match = matchPathAndTarget( + path, path_idx, target, current, flags, genus, ref); + match != nullptr) + return match; + } + current = current->parent(); + path_idx = 0; + } + + if (node && result) { + // Fall back to previously found section title + ref = result->m_ref; + if (targetType) + *targetType = result->m_type; + } + return node; +} + +/*! + First, the \a path is used to find a node. The \a path + matches some part of the node's fully quallified name. + If the \a target is not empty, it must match a target + in the matching node. If the matching of the \a path + and the \a target (if present) is successful, \a ref + is set from the \a target, and the pointer to the + matching node is returned. \a idx is the index into the + \a path where to begin the matching. The function is + recursive with idx being incremented for each recursive + call. + + The matching node must be of the correct \a genus, i.e. + either QML or C++, but \a genus can be set to \c DontCare. + \a flags indicates whether to search base classes and + whether to search for an enum value. \a node points to + the node where the search should begin, assuming the + \a path is a not a fully-qualified name. \a node is + most often the root of this Tree. + */ +const Node *Tree::matchPathAndTarget(const QStringList &path, int idx, const QString &target, + const Node *node, int flags, Node::Genus genus, + QString &ref) const +{ + /* + If the path has been matched, then if there is a target, + try to match the target. If there is a target, but you + can't match it at the end of the path, give up; return 0. + */ + if (idx == path.size()) { + if (!target.isEmpty()) { + ref = getRef(target, node); + if (ref.isEmpty()) + return nullptr; + } + if (node->isFunction() && node->name() == node->parent()->name()) + node = node->parent(); + return node; + } + + QString name = path.at(idx); + if (node->isAggregate()) { + NodeVector nodes; + static_cast<const Aggregate *>(node)->findChildren(name, nodes); + for (const auto *child : std::as_const(nodes)) { + if (genus != Node::DontCare && !(genus & child->genus())) + continue; + const Node *t = matchPathAndTarget(path, idx + 1, target, child, flags, genus, ref); + if (t && !t->isPrivate()) + return t; + } + } + if (target.isEmpty() && (flags & SearchEnumValues)) { + const auto *enumNode = node->isAggregate() ? + findEnumNode(nullptr, node, path, idx) : + findEnumNode(node, nullptr, path, idx); + if (enumNode) + return enumNode; + } + if (((genus == Node::CPP) || (genus == Node::DontCare)) && node->isClassNode() + && (flags & SearchBaseClasses)) { + const ClassList bases = allBaseClasses(static_cast<const ClassNode *>(node)); + for (const auto *base : bases) { + const Node *t = matchPathAndTarget(path, idx, target, base, flags, genus, ref); + if (t && !t->isPrivate()) + return t; + if (target.isEmpty() && (flags & SearchEnumValues)) { + if ((t = findEnumNode(base->findChildNode(path.at(idx), genus, flags), base, path, idx))) + return t; + } + } + } + return nullptr; +} + +/*! + Searches the tree for a node that matches the \a path. The + search begins at \a start but can move up the parent chain + recursively if no match is found. The \a flags are used to + restrict the search. + */ +const Node *Tree::findNode(const QStringList &path, const Node *start, int flags, + Node::Genus genus) const +{ + const Node *current = start; + if (current == nullptr) + current = root(); + + do { + const Node *node = current; + int i; + int start_idx = 0; + + /* + If the path contains one or two double colons ("::"), + check first to see if the first two path strings refer + to a QML element. If they do, path[0] will be the QML + module identifier, and path[1] will be the QML type. + If the answer is yes, the reference identifies a QML + type node. + */ + if (((genus == Node::QML) || (genus == Node::DontCare)) && (path.size() >= 2) + && !path[0].isEmpty()) { + QmlTypeNode *qcn = lookupQmlType(QString(path[0] + "::" + path[1])); + if (qcn != nullptr) { + node = qcn; + if (path.size() == 2) + return node; + start_idx = 2; + } + } + + for (i = start_idx; i < path.size(); ++i) { + if (node == nullptr || !node->isAggregate()) + break; + + // Clear the TypesOnly flag until the last path segment, as e.g. namespaces are not + // types. We also ignore module nodes as they are not aggregates and thus have no + // children. + int tmpFlags = (i < path.size() - 1) ? (flags & ~TypesOnly) | IgnoreModules : flags; + + const Node *next = static_cast<const Aggregate *>(node)->findChildNode(path.at(i), + genus, tmpFlags); + const Node *enumNode = (flags & SearchEnumValues) ? + findEnumNode(next, node, path, i) : nullptr; + + if (enumNode) + return enumNode; + + + if (!next && ((genus == Node::CPP) || (genus == Node::DontCare)) + && node->isClassNode() && (flags & SearchBaseClasses)) { + const ClassList bases = allBaseClasses(static_cast<const ClassNode *>(node)); + for (const auto *base : bases) { + next = base->findChildNode(path.at(i), genus, tmpFlags); + if (flags & SearchEnumValues) + if ((enumNode = findEnumNode(next, base, path, i))) + return enumNode; + if (next) + break; + } + } + node = next; + } + if ((node != nullptr) && i == path.size()) + return node; + current = current->parent(); + } while (current != nullptr); + + return nullptr; +} + + +/*! + \internal + + Helper function to return an enum that matches the \a path at a specified \a offset. + If \a node is a valid enum node, the enum name is assumed to be included in the path + (i.e, a scoped enum). Otherwise, query the \a aggregate (typically, the class node) + for enum node that includes the value at the last position in \a path. + */ +const Node *Tree::findEnumNode(const Node *node, const Node *aggregate, const QStringList &path, int offset) const +{ + // Scoped enum (path ends in enum_name :: enum_value) + if (node && node->isEnumType() && offset == path.size() - 1) { + const auto *en = static_cast<const EnumNode*>(node); + if (en->isScoped() && en->hasItem(path.last())) + return en; + } + + // Standard enum (path ends in class_name :: enum_value) + return (!node && aggregate && offset == path.size() - 1) ? + static_cast<const Aggregate *>(aggregate)->findEnumNodeForValue(path.last()) : + nullptr; +} + +/*! + This function searches for a node with a canonical title + constructed from \a target. If the node it finds is \a node, + it returns the ref from that node. Otherwise it returns an + empty string. + */ +QString Tree::getRef(const QString &target, const Node *node) const +{ + auto it = m_nodesByTargetTitle.constFind(target); + if (it != m_nodesByTargetTitle.constEnd()) { + do { + if (it.value()->m_node == node) + return it.value()->m_ref; + ++it; + } while (it != m_nodesByTargetTitle.constEnd() && it.key() == target); + } + QString key = Utilities::asAsciiPrintable(target); + it = m_nodesByTargetRef.constFind(key); + if (it != m_nodesByTargetRef.constEnd()) { + do { + if (it.value()->m_node == node) + return it.value()->m_ref; + ++it; + } while (it != m_nodesByTargetRef.constEnd() && it.key() == key); + } + return QString(); +} + +/*! + Inserts a new target into the target table. \a name is the + key. The target record contains the \a type, a pointer to + the \a node, the \a priority. and a canonicalized form of + the \a name, which is later used. + */ +void Tree::insertTarget(const QString &name, const QString &title, TargetRec::TargetType type, + Node *node, int priority) +{ + auto *target = new TargetRec(name, type, node, priority); + m_nodesByTargetRef.insert(name, target); + m_nodesByTargetTitle.insert(title, target); +} + +/*! + \internal + + \a root is the root node of the tree to resolve targets for. This function + traverses the tree starting from the root node and processes each child + node. If the child node is an aggregate node, this function is called + recursively on the child node. + */ +void Tree::resolveTargets(Aggregate *root) +{ + for (auto *child : root->childNodes()) { + addToPageNodeByTitleMap(child); + populateTocSectionTargetMap(child); + addKeywordsToTargetMaps(child); + addTargetsToTargetMap(child); + + if (child->isAggregate()) + resolveTargets(static_cast<Aggregate *>(child)); + } +} + +/*! + \internal + + Updates the target maps for targets associated with the given \a node. + */ +void Tree::addTargetsToTargetMap(Node *node) { + if (!node || !node->doc().hasTargets()) + return; + + for (Atom *i : std::as_const(node->doc().targets())) { + const QString ref = refForAtom(i); + const QString title = i->string(); + if (!ref.isEmpty() && !title.isEmpty()) { + QString key = Utilities::asAsciiPrintable(title); + auto *target = new TargetRec(ref, TargetRec::Target, node, 2); + m_nodesByTargetRef.insert(key, target); + m_nodesByTargetTitle.insert(title, target); + } + } +} + +/*! + \internal + + Updates the target maps for keywords associated with the given \a node. + */ +void Tree::addKeywordsToTargetMaps(Node *node) { + if (!node->doc().hasKeywords()) + return; + + for (Atom *i : std::as_const(node->doc().keywords())) { + QString ref = refForAtom(i); + QString title = i->string(); + if (!ref.isEmpty() && !title.isEmpty()) { + auto *target = new TargetRec(ref, TargetRec::Keyword, node, 1); + m_nodesByTargetRef.insert(Utilities::asAsciiPrintable(title), target); + m_nodesByTargetTitle.insert(title, target); + } + } +} + +/*! + \internal + + Populates the map of targets for each section in the table of contents for + the given \a node while ensuring that each target has a unique reference. + */ +void Tree::populateTocSectionTargetMap(Node *node) { + if (!node || !node->doc().hasTableOfContents()) + return; + + QStack<Atom *> tocLevels; + QSet<QString> anchors; + + qsizetype index = 0; + + for (Atom *atom: std::as_const(node->doc().tableOfContents())) { + while (!tocLevels.isEmpty() && tocLevels.top()->string().toInt() >= atom->string().toInt()) + tocLevels.pop(); + + tocLevels.push(atom); + + QString ref = refForAtom(atom); + const QString &title = Text::sectionHeading(atom).toString(); + if (ref.isEmpty() || title.isEmpty()) + continue; + + if (anchors.contains(ref)) { + QStringList refParts; + for (const auto tocLevel : tocLevels) + refParts << refForAtom(tocLevel); + + refParts << QString::number(index); + ref = refParts.join(QLatin1Char('-')); + } + + anchors.insert(ref); + if (atom->next(Atom::SectionHeadingLeft)) + atom->next()->append(ref); + ++index; + + const QString &key = Utilities::asAsciiPrintable(title); + auto *target = new TargetRec(ref, TargetRec::Contents, node, 3); + m_nodesByTargetRef.insert(key, target); + m_nodesByTargetTitle.insert(title, target); + } +} + +/*! + \internal + + Checks if the \a node's title is registered in the page nodes by title map. + If not, it stores the page node in the map. + */ +void Tree::addToPageNodeByTitleMap(Node *node) { + if (!node || !node->isTextPageNode()) + return; + + auto *pageNode = static_cast<PageNode *>(node); + QString key = pageNode->title(); + if (key.isEmpty()) + return; + + if (key.contains(QChar(' '))) + key = Utilities::asAsciiPrintable(key); + const QList<PageNode *> nodes = m_pageNodesByTitle.values(key); + + bool alreadyThere = std::any_of(nodes.cbegin(), nodes.cend(), [&](const auto &knownNode) { + return knownNode->isExternalPage() && knownNode->name() == pageNode->name(); + }); + + if (!alreadyThere) + m_pageNodesByTitle.insert(key, pageNode); +} + +/*! + Searches for a \a target anchor, matching the given \a genus, and returns + the associated TargetRec instance. + */ +const TargetRec *Tree::findUnambiguousTarget(const QString &target, Node::Genus genus) const +{ + auto findBestCandidate = [&](const TargetMap &tgtMap, const QString &key) { + TargetRec *best = nullptr; + auto [it, end] = tgtMap.equal_range(key); + while (it != end) { + TargetRec *candidate = it.value(); + if ((genus == Node::DontCare) || (genus & candidate->genus())) { + if (!best || (candidate->m_priority < best->m_priority)) + best = candidate; + } + ++it; + } + return best; + }; + + TargetRec *bestTarget = findBestCandidate(m_nodesByTargetTitle, target); + if (!bestTarget) + bestTarget = findBestCandidate(m_nodesByTargetRef, Utilities::asAsciiPrintable(target)); + + return bestTarget; +} + +/*! + This function searches for a node with the specified \a title. + */ +const PageNode *Tree::findPageNodeByTitle(const QString &title) const +{ + PageNodeMultiMap::const_iterator it; + if (title.contains(QChar(' '))) + it = m_pageNodesByTitle.constFind(Utilities::asAsciiPrintable(title)); + else + it = m_pageNodesByTitle.constFind(title); + if (it != m_pageNodesByTitle.constEnd()) { + /* + Reporting all these duplicate section titles is probably + overkill. We should report the duplicate file and let + that suffice. + */ + PageNodeMultiMap::const_iterator j = it; + ++j; + if (j != m_pageNodesByTitle.constEnd() && j.key() == it.key()) { + while (j != m_pageNodesByTitle.constEnd()) { + if (j.key() == it.key() && j.value()->url().isEmpty()) { + break; // Just report one duplicate for now. + } + ++j; + } + if (j != m_pageNodesByTitle.cend()) { + it.value()->location().warning("This page title exists in more than one file: " + + title); + j.value()->location().warning("[It also exists here]"); + } + } + return it.value(); + } + return nullptr; +} + +/*! + Returns a canonical title for the \a atom, if the \a atom + is a SectionLeft, SectionHeadingLeft, Keyword, or Target. + */ +QString Tree::refForAtom(const Atom *atom) +{ + Q_ASSERT(atom); + + switch (atom->type()) { + case Atom::SectionLeft: + atom = atom->next(); + [[fallthrough]]; + case Atom::SectionHeadingLeft: + if (atom->count() == 2) + return atom->string(1); + return Utilities::asAsciiPrintable(Text::sectionHeading(atom).toString()); + case Atom::Target: + [[fallthrough]]; + case Atom::Keyword: + return Utilities::asAsciiPrintable(atom->string()); + default: + return {}; + } +} + +/*! + \fn const CNMap &Tree::groups() const + Returns a const reference to the collection of all + group nodes. +*/ + +/*! + \fn const ModuleMap &Tree::modules() const + Returns a const reference to the collection of all + module nodes. +*/ + +/*! + \fn const QmlModuleMap &Tree::qmlModules() const + Returns a const reference to the collection of all + QML module nodes. +*/ + +/*! + Returns a pointer to the collection map specified by \a type. + Returns null if \a type is not specified. + */ +CNMap *Tree::getCollectionMap(Node::NodeType type) +{ + switch (type) { + case Node::Group: + return &m_groups; + case Node::Module: + return &m_modules; + case Node::QmlModule: + return &m_qmlModules; + default: + break; + } + return nullptr; +} + +/*! + Searches this tree for a collection named \a name with the + specified \a type. If the collection is found, a pointer + to it is returned. If a collection is not found, null is + returned. + */ +CollectionNode *Tree::getCollection(const QString &name, Node::NodeType type) +{ + CNMap *map = getCollectionMap(type); + if (map) { + auto it = map->constFind(name); + if (it != map->cend()) + return it.value(); + } + return nullptr; +} + +/*! + Find the group, module, or QML module named \a name and return a + pointer to that collection node. \a type specifies which kind of + collection node you want. If a collection node with the specified \a + name and \a type is not found, a new one is created, and the pointer + to the new one is returned. + + If a new collection node is created, its parent is the tree + root, and the new collection node is marked \e{not seen}. + + \a genus must be specified, i.e. it must not be \c{DontCare}. + If it is \c{DontCare}, 0 is returned, which is a programming + error. + */ +CollectionNode *Tree::findCollection(const QString &name, Node::NodeType type) +{ + CNMap *m = getCollectionMap(type); + if (!m) // error + return nullptr; + auto it = m->constFind(name); + if (it != m->cend()) + return it.value(); + CollectionNode *cn = new CollectionNode(type, root(), name); + cn->markNotSeen(); + m->insert(name, cn); + return cn; +} + +/*! \fn CollectionNode *Tree::findGroup(const QString &name) + Find the group node named \a name and return a pointer + to it. If the group node is not found, add a new group + node named \a name and return a pointer to the new one. + + If a new group node is added, its parent is the tree root, + and the new group node is marked \e{not seen}. + */ + +/*! \fn CollectionNode *Tree::findModule(const QString &name) + Find the module node named \a name and return a pointer + to it. If a matching node is not found, add a new module + node named \a name and return a pointer to that one. + + If a new module node is added, its parent is the tree root, + and the new module node is marked \e{not seen}. + */ + +/*! \fn CollectionNode *Tree::findQmlModule(const QString &name) + Find the QML module node named \a name and return a pointer + to it. If a matching node is not found, add a new QML module + node named \a name and return a pointer to that one. + + If a new QML module node is added, its parent is the tree root, + and the new node is marked \e{not seen}. + */ + +/*! \fn CollectionNode *Tree::addGroup(const QString &name) + Looks up the group node named \a name in the collection + of all group nodes. If a match is found, a pointer to the + node is returned. Otherwise, a new group node named \a name + is created and inserted into the collection, and the pointer + to that node is returned. + */ + +/*! \fn CollectionNode *Tree::addModule(const QString &name) + Looks up the module node named \a name in the collection + of all module nodes. If a match is found, a pointer to the + node is returned. Otherwise, a new module node named \a name + is created and inserted into the collection, and the pointer + to that node is returned. + */ + +/*! \fn CollectionNode *Tree::addQmlModule(const QString &name) + Looks up the QML module node named \a name in the collection + of all QML module nodes. If a match is found, a pointer to the + node is returned. Otherwise, a new QML module node named \a name + is created and inserted into the collection, and the pointer + to that node is returned. + */ + +/*! + Looks up the group node named \a name in the collection + of all group nodes. If a match is not found, a new group + node named \a name is created and inserted into the collection. + Then append \a node to the group's members list, and append the + group name to the list of group names in \a node. The parent of + \a node is not changed by this function. Returns a pointer to + the group node. + */ +CollectionNode *Tree::addToGroup(const QString &name, Node *node) +{ + CollectionNode *cn = findGroup(name); + if (!node->isInternal()) { + cn->addMember(node); + node->appendGroupName(name); + } + return cn; +} + +/*! + Looks up the module node named \a name in the collection + of all module nodes. If a match is not found, a new module + node named \a name is created and inserted into the collection. + Then append \a node to the module's members list. The parent of + \a node is not changed by this function. Returns the module node. + */ +CollectionNode *Tree::addToModule(const QString &name, Node *node) +{ + CollectionNode *cn = findModule(name); + cn->addMember(node); + node->setPhysicalModuleName(name); + return cn; +} + +/*! + Looks up the QML module named \a name. If it isn't there, + create it. Then append \a node to the QML module's member + list. The parent of \a node is not changed by this function. + Returns the pointer to the QML module node. + */ +CollectionNode *Tree::addToQmlModule(const QString &name, Node *node) +{ + QStringList qmid; + QStringList dotSplit; + QStringList blankSplit = name.split(QLatin1Char(' ')); + qmid.append(blankSplit[0]); + if (blankSplit.size() > 1) { + qmid.append(blankSplit[0] + blankSplit[1]); + dotSplit = blankSplit[1].split(QLatin1Char('.')); + qmid.append(blankSplit[0] + dotSplit[0]); + } + + CollectionNode *cn = findQmlModule(blankSplit[0]); + cn->addMember(node); + node->setQmlModule(cn); + if (node->isQmlType()) { + QmlTypeNode *n = static_cast<QmlTypeNode *>(node); + for (int i = 0; i < qmid.size(); ++i) { + QString key = qmid[i] + "::" + node->name(); + insertQmlType(key, n); + } + } + return cn; +} + +/*! + If the QML type map does not contain \a key, insert node + \a n with the specified \a key. + */ +void Tree::insertQmlType(const QString &key, QmlTypeNode *n) +{ + if (!m_qmlTypeMap.contains(key)) + m_qmlTypeMap.insert(key, n); +} + +/*! + Finds the function node with the specifried name \a path that + also has the specified \a parameters and returns a pointer to + the first matching function node if one is found. + + This function begins searching the tree at \a relative for + the \l {FunctionNode} {function node} identified by \a path + that has the specified \a parameters. The \a flags are + used to restrict the search. If a matching node is found, a + pointer to it is returned. Otherwise, nullis returned. If + \a relative is ull, the search begins at the tree root. + */ +const FunctionNode *Tree::findFunctionNode(const QStringList &path, const Parameters ¶meters, + const Node *relative, Node::Genus genus) const +{ + if (path.size() == 3 && !path[0].isEmpty() + && ((genus == Node::QML) || (genus == Node::DontCare))) { + QmlTypeNode *qcn = lookupQmlType(QString(path[0] + "::" + path[1])); + if (qcn == nullptr) { + QStringList p(path[1]); + Node *n = findNodeByNameAndType(p, &Node::isQmlType); + if ((n != nullptr) && n->isQmlType()) + qcn = static_cast<QmlTypeNode *>(n); + } + if (qcn != nullptr) + return static_cast<const FunctionNode *>(qcn->findFunctionChild(path[2], parameters)); + } + + if (relative == nullptr) + relative = root(); + else if (genus != Node::DontCare) { + if (!(genus & relative->genus())) + relative = root(); + } + + do { + Node *node = const_cast<Node *>(relative); + int i; + + for (i = 0; i < path.size(); ++i) { + if (node == nullptr || !node->isAggregate()) + break; + + Aggregate *aggregate = static_cast<Aggregate *>(node); + Node *next = nullptr; + if (i == path.size() - 1) + next = aggregate->findFunctionChild(path.at(i), parameters); + else + next = aggregate->findChildNode(path.at(i), genus); + + if ((next == nullptr) && aggregate->isClassNode()) { + const ClassList bases = allBaseClasses(static_cast<const ClassNode *>(aggregate)); + for (auto *base : bases) { + if (i == path.size() - 1) + next = base->findFunctionChild(path.at(i), parameters); + else + next = base->findChildNode(path.at(i), genus); + + if (next != nullptr) + break; + } + } + + node = next; + } // for (i = 0; i < path.size(); ++i) + + if (node && i == path.size() && node->isFunction()) { + // A function node was found at the end of the path. + // If it is not marked private, return it. If it is + // marked private, then if it overrides a function, + // find that function instead because it might not + // be marked private. If all the overloads are + // marked private, return the original function node. + // This should be replace with findOverriddenFunctionNode(). + const FunctionNode *fn = static_cast<const FunctionNode *>(node); + const FunctionNode *FN = fn; + while (FN->isPrivate() && !FN->overridesThis().isEmpty()) { + QStringList path = FN->overridesThis().split("::"); + FN = m_qdb->findFunctionNode(path, parameters, relative, genus); + if (FN == nullptr) + break; + if (!FN->isPrivate()) + return FN; + } + return fn; + } + relative = relative->parent(); + } while (relative); + return nullptr; +} + +/*! + Search this tree recursively from \a parent to find a function + node with the specified \a tag. If no function node is found + with the required \a tag, return 0. + */ +FunctionNode *Tree::findFunctionNodeForTag(const QString &tag, Aggregate *parent) +{ + if (parent == nullptr) + parent = root(); + const NodeList &children = parent->childNodes(); + for (Node *n : children) { + if (n != nullptr && n->isFunction() && n->hasTag(tag)) + return static_cast<FunctionNode *>(n); + } + for (Node *n : children) { + if (n != nullptr && n->isAggregate()) { + n = findFunctionNodeForTag(tag, static_cast<Aggregate *>(n)); + if (n != nullptr) + return static_cast<FunctionNode *>(n); + } + } + return nullptr; +} + +/*! + There should only be one macro node for macro name \a t. + The macro node is not built until the \macro command is seen. + */ +FunctionNode *Tree::findMacroNode(const QString &t, const Aggregate *parent) +{ + if (parent == nullptr) + parent = root(); + const NodeList &children = parent->childNodes(); + for (Node *n : children) { + if (n != nullptr && (n->isMacro() || n->isFunction()) && n->name() == t) + return static_cast<FunctionNode *>(n); + } + for (Node *n : children) { + if (n != nullptr && n->isAggregate()) { + FunctionNode *fn = findMacroNode(t, static_cast<Aggregate *>(n)); + if (fn != nullptr) + return fn; + } + } + return nullptr; +} + +/*! + Add the class and struct names in \a arg to the \e {don't document} + map. + */ +void Tree::addToDontDocumentMap(QString &arg) +{ + arg.remove(QChar('(')); + arg.remove(QChar(')')); + QString t = arg.simplified(); + QStringList sl = t.split(QChar(' ')); + if (sl.isEmpty()) + return; + for (const QString &s : sl) { + if (!m_dontDocumentMap.contains(s)) + m_dontDocumentMap.insert(s, nullptr); + } +} + +/*! + The \e {don't document} map has been loaded with the names + of classes and structs in the current module that are not + documented and should not be documented. Now traverse the + map, and for each class or struct name, find the class node + that represents that class or struct and mark it with the + \C DontDocument status. + + This results in a map of the class and struct nodes in the + module that are in the public API but are not meant to be + used by anyone. They are only used internally, but for one + reason or another, they must have public visibility. + */ +void Tree::markDontDocumentNodes() +{ + for (auto it = m_dontDocumentMap.begin(); it != m_dontDocumentMap.end(); ++it) { + Aggregate *node = findAggregate(it.key()); + if (node != nullptr) + node->setStatus(Node::DontDocument); + } +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/tree.h b/src/qdoc/qdoc/src/qdoc/tree.h new file mode 100644 index 000000000..d0bed25d6 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/tree.h @@ -0,0 +1,183 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef TREE_H +#define TREE_H + +#include "examplenode.h" +#include "namespacenode.h" +#include "node.h" +#include "propertynode.h" +#include "proxynode.h" +#include "qmltypenode.h" + +#include <QtCore/qstack.h> + +#include <utility> + +QT_BEGIN_NAMESPACE + +class CollectionNode; +class FunctionNode; +class QDocDatabase; + +struct TargetRec +{ +public: + enum TargetType { Unknown, Target, Keyword, Contents }; + + TargetRec(QString name, TargetRec::TargetType type, Node *node, int priority) + : m_node(node), m_ref(std::move(name)), m_type(type), m_priority(priority) + { + // Discard the dedicated ref for keywords - they always + // link to the top of the QDoc comment they appear in + if (type == Keyword) + m_ref.clear(); + } + + [[nodiscard]] bool isEmpty() const { return m_ref.isEmpty(); } + [[nodiscard]] Node::Genus genus() const { return (m_node ? m_node->genus() : Node::DontCare); } + + Node *m_node { nullptr }; + QString m_ref {}; + TargetType m_type {}; + int m_priority {}; +}; + +typedef QMultiMap<QString, TargetRec *> TargetMap; +typedef QMultiMap<QString, PageNode *> PageNodeMultiMap; +typedef QMap<QString, QmlTypeNode *> QmlTypeMap; +typedef QMultiMap<QString, const ExampleNode *> ExampleNodeMap; + +class Tree +{ + friend class QDocForest; + friend class QDocDatabase; + +private: // Note the constructor and destructor are private. + typedef QMap<PropertyNode::FunctionRole, QString> RoleMap; + typedef QMap<PropertyNode *, RoleMap> PropertyMap; + + Tree(const QString &camelCaseModuleName, QDocDatabase *qdb); + ~Tree(); + +public: // Of necessity, a few public functions remain. + [[nodiscard]] Node *findNodeByNameAndType(const QStringList &path, + bool (Node::*isMatch)() const) const; + + [[nodiscard]] const QString &camelCaseModuleName() const { return m_camelCaseModuleName; } + [[nodiscard]] const QString &physicalModuleName() const { return m_physicalModuleName; } + [[nodiscard]] const QString &indexFileName() const { return m_indexFileName; } + [[nodiscard]] const QString &indexTitle() const { return m_indexTitle; } + void setIndexTitle(const QString &t) { m_indexTitle = t; } + NodeList &proxies() { return m_proxies; } + void appendProxy(ProxyNode *t) { m_proxies.append(t); } + void addToDontDocumentMap(QString &arg); + void markDontDocumentNodes(); + static QString refForAtom(const Atom *atom); + +private: // The rest of the class is private. + Aggregate *findAggregate(const QString &name); + [[nodiscard]] Node *findNodeForInclude(const QStringList &path) const; + ClassNode *findClassNode(const QStringList &path, const Node *start = nullptr) const; + [[nodiscard]] NamespaceNode *findNamespaceNode(const QStringList &path) const; + const FunctionNode *findFunctionNode(const QStringList &path, const Parameters ¶meters, + const Node *relative, Node::Genus genus) const; + Node *findNodeRecursive(const QStringList &path, int pathIndex, const Node *start, + bool (Node::*)() const) const; + const Node *findNodeForTarget(const QStringList &path, const QString &target, const Node *node, + int flags, Node::Genus genus, QString &ref, + TargetRec::TargetType *targetType = nullptr) const; + const Node *matchPathAndTarget(const QStringList &path, int idx, const QString &target, + const Node *node, int flags, Node::Genus genus, + QString &ref) const; + + const Node *findNode(const QStringList &path, const Node *relative, int flags, + Node::Genus genus) const; + + Aggregate *findRelatesNode(const QStringList &path); + const Node *findEnumNode(const Node *node, const Node *aggregate, const QStringList &path, int offset) const; + QString getRef(const QString &target, const Node *node) const; + void insertTarget(const QString &name, const QString &title, TargetRec::TargetType type, + Node *node, int priority); + void resolveTargets(Aggregate *root); + void addToPageNodeByTitleMap(Node *node); + void populateTocSectionTargetMap(Node *node); + void addKeywordsToTargetMaps(Node *node); + void addTargetsToTargetMap(Node *node); + + const TargetRec *findUnambiguousTarget(const QString &target, Node::Genus genus) const; + [[nodiscard]] const PageNode *findPageNodeByTitle(const QString &title) const; + + void addPropertyFunction(PropertyNode *property, const QString &funcName, + PropertyNode::FunctionRole funcRole); + void resolveBaseClasses(Aggregate *n); + void resolvePropertyOverriddenFromPtrs(Aggregate *n); + void resolveProperties(); + void resolveCppToQmlLinks(); + void resolveSince(Aggregate &aggregate); + void resolveEnumValueSince(EnumNode &en); + void removePrivateAndInternalBases(NamespaceNode *rootNode); + NamespaceNode *root() { return &m_root; } + [[nodiscard]] const NamespaceNode *root() const { return &m_root; } + + ClassList allBaseClasses(const ClassNode *classe) const; + + CNMap *getCollectionMap(Node::NodeType type); + [[nodiscard]] const CNMap &groups() const { return m_groups; } + [[nodiscard]] const CNMap &modules() const { return m_modules; } + [[nodiscard]] const CNMap &qmlModules() const { return m_qmlModules; } + + CollectionNode *getCollection(const QString &name, Node::NodeType type); + CollectionNode *findCollection(const QString &name, Node::NodeType type); + + CollectionNode *findGroup(const QString &name) { return findCollection(name, Node::Group); } + CollectionNode *findModule(const QString &name) { return findCollection(name, Node::Module); } + CollectionNode *findQmlModule(const QString &name) + { + return findCollection(name, Node::QmlModule); + } + + CollectionNode *addGroup(const QString &name) { return findGroup(name); } + CollectionNode *addModule(const QString &name) { return findModule(name); } + CollectionNode *addQmlModule(const QString &name) { return findQmlModule(name); } + + CollectionNode *addToGroup(const QString &name, Node *node); + CollectionNode *addToModule(const QString &name, Node *node); + CollectionNode *addToQmlModule(const QString &name, Node *node); + + [[nodiscard]] QmlTypeNode *lookupQmlType(const QString &name) const + { + return m_qmlTypeMap.value(name); + } + void insertQmlType(const QString &key, QmlTypeNode *n); + void addExampleNode(ExampleNode *n) { m_exampleNodeMap.insert(n->title(), n); } + ExampleNodeMap &exampleNodeMap() { return m_exampleNodeMap; } + void setIndexFileName(const QString &t) { m_indexFileName = t; } + + FunctionNode *findFunctionNodeForTag(const QString &tag, Aggregate *parent = nullptr); + FunctionNode *findMacroNode(const QString &t, const Aggregate *parent = nullptr); + +private: + QString m_camelCaseModuleName {}; + QString m_physicalModuleName {}; + QString m_indexFileName {}; + QString m_indexTitle {}; + QDocDatabase *m_qdb { nullptr }; + NamespaceNode m_root; + PropertyMap m_unresolvedPropertyMap {}; + PageNodeMultiMap m_pageNodesByTitle {}; + TargetMap m_nodesByTargetRef {}; + TargetMap m_nodesByTargetTitle {}; + CNMap m_groups {}; + CNMap m_modules {}; + CNMap m_qmlModules {}; + QmlTypeMap m_qmlTypeMap {}; + ExampleNodeMap m_exampleNodeMap {}; + NodeList m_proxies {}; + NodeMap m_dontDocumentMap {}; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/typedefnode.cpp b/src/qdoc/qdoc/src/qdoc/typedefnode.cpp new file mode 100644 index 000000000..997e570d3 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/typedefnode.cpp @@ -0,0 +1,55 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "typedefnode.h" + +#include "aggregate.h" + +QT_BEGIN_NAMESPACE + +/*! + \class TypedefNode + */ + +/*! + */ +void TypedefNode::setAssociatedEnum(const EnumNode *enume) +{ + m_associatedEnum = enume; +} + +/*! + Clone this node on the heap and make the clone a child of + \a parent. + + Returns the pointer to the clone. + */ +Node *TypedefNode::clone(Aggregate *parent) +{ + auto *tn = new TypedefNode(*this); // shallow copy + tn->setParent(nullptr); + parent->addChild(tn); + + return tn; +} + +/*! + \class TypeAliasNode + */ + +/*! + Clone this node on the heap and make the clone a child of + \a parent. + + Returns the pointer to the clone. + */ +Node *TypeAliasNode::clone(Aggregate *parent) +{ + auto *tan = new TypeAliasNode(*this); // shallow copy + tan->setParent(nullptr); + parent->addChild(tan); + + return tan; +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/typedefnode.h b/src/qdoc/qdoc/src/qdoc/typedefnode.h new file mode 100644 index 000000000..839cd6a0b --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/typedefnode.h @@ -0,0 +1,54 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef TYPEDEFNODE_H +#define TYPEDEFNODE_H + +#include "enumnode.h" +#include "node.h" + +#include <QtCore/qglobal.h> +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +class Aggregate; + +class TypedefNode : public Node +{ +public: + TypedefNode(Aggregate *parent, const QString &name, NodeType type = Typedef) + : Node(type, parent, name) + { + } + + bool hasAssociatedEnum() const { return m_associatedEnum != nullptr; } + const EnumNode *associatedEnum() const { return m_associatedEnum; } + Node *clone(Aggregate *parent) override; + +private: + void setAssociatedEnum(const EnumNode *t); + + friend class EnumNode; + + const EnumNode *m_associatedEnum { nullptr }; +}; + +class TypeAliasNode : public TypedefNode +{ +public: + TypeAliasNode(Aggregate *parent, const QString &name, const QString &aliasedType) + : TypedefNode(parent, name, NodeType::TypeAlias), m_aliasedType(aliasedType) + { + } + + const QString &aliasedType() const { return m_aliasedType; } + Node *clone(Aggregate *parent) override; + +private: + QString m_aliasedType {}; +}; + +QT_END_NAMESPACE + +#endif // TYPEDEFNODE_H diff --git a/src/qdoc/qdoc/src/qdoc/utilities.cpp b/src/qdoc/qdoc/src/qdoc/utilities.cpp new file mode 100644 index 000000000..2825804d6 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/utilities.cpp @@ -0,0 +1,254 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <QtCore/qprocess.h> +#include <QCryptographicHash> +#include "location.h" +#include "utilities.h" + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcQdoc, "qt.qdoc") +Q_LOGGING_CATEGORY(lcQdocClang, "qt.qdoc.clang") + +/*! + \namespace Utilities + \internal + \brief This namespace holds QDoc-internal utility methods. + */ +namespace Utilities { +static inline void setDebugEnabled(bool value) +{ + const_cast<QLoggingCategory &>(lcQdoc()).setEnabled(QtDebugMsg, value); + const_cast<QLoggingCategory &>(lcQdocClang()).setEnabled(QtDebugMsg, value); +} + +void startDebugging(const QString &message) +{ + setDebugEnabled(true); + qCDebug(lcQdoc, "START DEBUGGING: %ls", qUtf16Printable(message)); +} + +void stopDebugging(const QString &message) +{ + qCDebug(lcQdoc, "STOP DEBUGGING: %ls", qUtf16Printable(message)); + setDebugEnabled(false); +} + +bool debugging() +{ + return lcQdoc().isEnabled(QtDebugMsg); +} + +/*! + \internal + Convenience method that's used to get the correct punctuation character for + the words at \a wordPosition in a list of \a numberOfWords length. + For the last position in the list, returns "." (full stop). For any other + word, this method calls comma(). + + \sa comma() + */ +QString separator(qsizetype wordPosition, qsizetype numberOfWords) +{ + static QString terminator = QStringLiteral("."); + if (wordPosition == numberOfWords - 1) + return terminator; + else + return comma(wordPosition, numberOfWords); +} + +/*! + \internal + Convenience method that's used to get the correct punctuation character for + the words at \a wordPosition in a list of \a numberOfWords length. + + For a list of length one, returns an empty QString. For a list of length + two, returns the string " and ". For any length beyond two, returns the + string ", " until the last element, which returns ", and ". + + \sa comma() + */ +QString comma(qsizetype wordPosition, qsizetype numberOfWords) +{ + if (wordPosition == numberOfWords - 1) + return QString(); + if (numberOfWords == 2) + return QStringLiteral(" and "); + if (wordPosition == 0 || wordPosition < numberOfWords - 2) + return QStringLiteral(", "); + return QStringLiteral(", and "); +} + +/*! + \brief Returns an ascii-printable representation of \a str. + + Replace non-ascii-printable characters in \a str from a subset of such + characters. The subset includes alphanumeric (alnum) characters + ([a-zA-Z0-9]), space, punctuation characters, and common symbols. Non-alnum + characters in this subset are replaced by a single hyphen. Leading, + trailing, and consecutive hyphens are removed, such that the resulting + string does not start or end with a hyphen. All characters are converted to + lowercase. + + If any character in \a str is non-latin, or latin and not found in the + aforementioned subset (e.g. 'ß', 'Ã¥', or 'ö'), a hash of \a str is appended + to the final string. + + Returns a string that is normalized for use where ascii-printable strings + are required, such as file names or fragment identifiers in URLs. + + The implementation is equivalent to: + + \code + name.replace(QRegularExpression("[^A-Za-z0-9]+"), " "); + name = name.simplified(); + name.replace(QLatin1Char(' '), QLatin1Char('-')); + name = name.toLower(); + \endcode + + However, it has been measured to be approximately four times faster. +*/ +QString asAsciiPrintable(const QString &str) +{ + auto legal_ascii = [](const uint value) { + const uint start_ascii_subset{ 32 }; + const uint end_ascii_subset{ 126 }; + + return value >= start_ascii_subset && value <= end_ascii_subset; + }; + + QString result; + bool begun = false; + bool has_non_alnum_content{ false }; + + for (const auto &c : str) { + char16_t u = c.unicode(); + if (!legal_ascii(u)) + has_non_alnum_content = true; + if (u >= 'A' && u <= 'Z') + u += 'a' - 'A'; + if ((u >= 'a' && u <= 'z') || (u >= '0' && u <= '9')) { + result += QLatin1Char(u); + begun = true; + } else if (begun) { + result += QLatin1Char('-'); + begun = false; + } + } + if (result.endsWith(QLatin1Char('-'))) + result.chop(1); + + if (has_non_alnum_content) { + auto title_hash = QString::fromLocal8Bit( + QCryptographicHash::hash(str.toUtf8(), QCryptographicHash::Md5).toHex()); + title_hash.truncate(8); + if (!result.isEmpty()) + result.append(QLatin1Char('-')); + result.append(title_hash); + } + + return result; +} + +/*! + \internal +*/ +static bool runProcess(const QString &program, const QStringList &arguments, + QByteArray *stdOutIn, QByteArray *stdErrIn) +{ + QProcess process; + process.start(program, arguments, QProcess::ReadWrite); + if (!process.waitForStarted()) { + qCDebug(lcQdoc).nospace() << "Unable to start " << process.program() + << ": " << process.errorString(); + return false; + } + process.closeWriteChannel(); + const bool finished = process.waitForFinished(); + const QByteArray stdErr = process.readAllStandardError(); + if (stdErrIn) + *stdErrIn = stdErr; + if (stdOutIn) + *stdOutIn = process.readAllStandardOutput(); + + if (!finished) { + qCDebug(lcQdoc).nospace() << process.program() << " timed out: " << stdErr; + process.kill(); + return false; + } + + if (process.exitStatus() != QProcess::NormalExit) { + qCDebug(lcQdoc).nospace() << process.program() << " crashed: " << stdErr; + return false; + } + + if (process.exitCode() != 0) { + qCDebug(lcQdoc).nospace() << process.program() << " exited with " + << process.exitCode() << ": " << stdErr; + return false; + } + + return true; +} + +/*! + \internal +*/ +static QByteArray frameworkSuffix() { + return QByteArrayLiteral(" (framework directory)"); +} + +/*! + \internal + Determine the compiler's internal include paths from the output of + + \badcode + [clang++|g++] -E -x c++ - -v </dev/null + \endcode + + Output looks like: + + \badcode + #include <...> search starts here: + /usr/local/include + /System/Library/Frameworks (framework directory) + End of search list. + \endcode +*/ +QStringList getInternalIncludePaths(const QString &compiler) +{ + QStringList result; + QStringList arguments; + arguments << QStringLiteral("-E") << QStringLiteral("-x") << QStringLiteral("c++") + << QStringLiteral("-") << QStringLiteral("-v"); + QByteArray stdOut; + QByteArray stdErr; + if (!runProcess(compiler, arguments, &stdOut, &stdErr)) + return result; + const QByteArrayList stdErrLines = stdErr.split('\n'); + bool isIncludeDir = false; + for (const QByteArray &line : stdErrLines) { + if (isIncludeDir) { + if (line.startsWith(QByteArrayLiteral("End of search list"))) { + isIncludeDir = false; + } else { + QByteArray prefix("-I"); + QByteArray headerPath{line.trimmed()}; + if (headerPath.endsWith(frameworkSuffix())) { + headerPath.truncate(headerPath.size() - frameworkSuffix().size()); + prefix = QByteArrayLiteral("-F"); + } + result.append(QString::fromLocal8Bit(prefix + headerPath)); + } + } else if (line.startsWith(QByteArrayLiteral("#include <...> search starts here"))) { + isIncludeDir = true; + } + } + + return result; +} + +} // namespace Utilities + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/utilities.h b/src/qdoc/qdoc/src/qdoc/utilities.h new file mode 100644 index 000000000..0d485f650 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/utilities.h @@ -0,0 +1,28 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef UTILITIES_H +#define UTILITIES_H + +#include <QtCore/qstring.h> +#include <QtCore/qloggingcategory.h> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(lcQdoc) +Q_DECLARE_LOGGING_CATEGORY(lcQdocClang) + +namespace Utilities { +void startDebugging(const QString &message); +void stopDebugging(const QString &message); +bool debugging(); + +QString separator(qsizetype wordPosition, qsizetype numberOfWords); +QString comma(qsizetype wordPosition, qsizetype numberOfWords); +QString asAsciiPrintable(const QString &name); +QStringList getInternalIncludePaths(const QString &compiler); +} + +QT_END_NAMESPACE + +#endif // UTILITIES_H diff --git a/src/qdoc/qdoc/src/qdoc/variablenode.cpp b/src/qdoc/qdoc/src/qdoc/variablenode.cpp new file mode 100644 index 000000000..11c8363f3 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/variablenode.cpp @@ -0,0 +1,23 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "variablenode.h" + +QT_BEGIN_NAMESPACE + +/*! + Clone this node on the heap and make the clone a child of + \a parent. + + Returns a pointer to the clone. + */ +Node *VariableNode::clone(Aggregate *parent) +{ + auto *vn = new VariableNode(*this); // shallow copy + vn->setParent(nullptr); + parent->addChild(vn); + + return vn; +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/variablenode.h b/src/qdoc/qdoc/src/qdoc/variablenode.h new file mode 100644 index 000000000..7db1252fe --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/variablenode.h @@ -0,0 +1,44 @@ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef VARIABLENODE_H +#define VARIABLENODE_H + +#include "aggregate.h" +#include "node.h" + +#include <QtCore/qglobal.h> +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +class VariableNode : public Node +{ +public: + VariableNode(Aggregate *parent, const QString &name); + + void setLeftType(const QString &leftType) { m_leftType = leftType; } + void setRightType(const QString &rightType) { m_rightType = rightType; } + void setStatic(bool b) { m_static = b; } + + [[nodiscard]] const QString &leftType() const { return m_leftType; } + [[nodiscard]] const QString &rightType() const { return m_rightType; } + [[nodiscard]] QString dataType() const { return m_leftType + m_rightType; } + [[nodiscard]] bool isStatic() const override { return m_static; } + Node *clone(Aggregate *parent) override; + +private: + QString m_leftType {}; + QString m_rightType {}; + bool m_static { false }; +}; + +inline VariableNode::VariableNode(Aggregate *parent, const QString &name) + : Node(Variable, parent, name) +{ + setGenus(Node::CPP); +} + +QT_END_NAMESPACE + +#endif // VARIABLENODE_H diff --git a/src/qdoc/qdoc/src/qdoc/webxmlgenerator.cpp b/src/qdoc/qdoc/src/qdoc/webxmlgenerator.cpp new file mode 100644 index 000000000..c2cc38161 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/webxmlgenerator.cpp @@ -0,0 +1,903 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "webxmlgenerator.h" + +#include "aggregate.h" +#include "collectionnode.h" +#include "config.h" +#include "helpprojectwriter.h" +#include "node.h" +#include "propertynode.h" +#include "qdocdatabase.h" +#include "quoter.h" +#include "utilities.h" + +#include <QtCore/qxmlstream.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +static CodeMarker *marker_ = nullptr; + +WebXMLGenerator::WebXMLGenerator(FileResolver& file_resolver) : HtmlGenerator(file_resolver) {} + +void WebXMLGenerator::initializeGenerator() +{ + HtmlGenerator::initializeGenerator(); +} + +void WebXMLGenerator::terminateGenerator() +{ + HtmlGenerator::terminateGenerator(); +} + +QString WebXMLGenerator::format() +{ + return "WebXML"; +} + +QString WebXMLGenerator::fileExtension() const +{ + // As this is meant to be an intermediate format, + // use .html for internal references. The name of + // the output file is set separately in + // beginSubPage() calls. + return "html"; +} + +/*! + Most of the output is generated by QDocIndexFiles and the append() callback. + Some pages produce supplementary output while being generated, and that's + handled here. +*/ +qsizetype WebXMLGenerator::generateAtom(const Atom *atom, const Node *relative, CodeMarker *marker) +{ + if (m_supplement && currentWriter) + addAtomElements(*currentWriter.data(), atom, relative, marker); + return 0; +} + +void WebXMLGenerator::generateCppReferencePage(Aggregate *aggregate, CodeMarker * /* marker */) +{ + QByteArray data; + QXmlStreamWriter writer(&data); + writer.setAutoFormatting(true); + beginSubPage(aggregate, Generator::fileName(aggregate, "webxml")); + writer.writeStartDocument(); + writer.writeStartElement("WebXML"); + writer.writeStartElement("document"); + + generateIndexSections(writer, aggregate); + + writer.writeEndElement(); // document + writer.writeEndElement(); // WebXML + writer.writeEndDocument(); + + out() << data; + endSubPage(); +} + +void WebXMLGenerator::generatePageNode(PageNode *pn, CodeMarker * /* marker */) +{ + QByteArray data; + currentWriter.reset(new QXmlStreamWriter(&data)); + currentWriter->setAutoFormatting(true); + beginSubPage(pn, Generator::fileName(pn, "webxml")); + currentWriter->writeStartDocument(); + currentWriter->writeStartElement("WebXML"); + currentWriter->writeStartElement("document"); + + generateIndexSections(*currentWriter.data(), pn); + + currentWriter->writeEndElement(); // document + currentWriter->writeEndElement(); // WebXML + currentWriter->writeEndDocument(); + + out() << data; + endSubPage(); +} + +void WebXMLGenerator::generateExampleFilePage(const Node *en, ResolvedFile resolved_file, CodeMarker* /* marker */) +{ + // TODO: [generator-insufficient-structural-abstraction] + + QByteArray data; + QXmlStreamWriter writer(&data); + writer.setAutoFormatting(true); + beginSubPage(en, linkForExampleFile(resolved_file.get_query(), "webxml")); + writer.writeStartDocument(); + writer.writeStartElement("WebXML"); + writer.writeStartElement("document"); + writer.writeStartElement("page"); + writer.writeAttribute("name", resolved_file.get_query()); + writer.writeAttribute("href", linkForExampleFile(resolved_file.get_query())); + const QString title = exampleFileTitle(static_cast<const ExampleNode *>(en), resolved_file.get_query()); + writer.writeAttribute("title", title); + writer.writeAttribute("fulltitle", title); + writer.writeAttribute("subtitle", resolved_file.get_query()); + writer.writeStartElement("description"); + + if (Config::instance().get(CONFIG_LOCATIONINFO).asBool()) { + writer.writeAttribute("path", resolved_file.get_path()); + writer.writeAttribute("line", "0"); + writer.writeAttribute("column", "0"); + } + + Quoter quoter; + Doc::quoteFromFile(en->doc().location(), quoter, resolved_file); + QString code = quoter.quoteTo(en->location(), QString(), QString()); + writer.writeTextElement("code", trimmedTrailing(code, QString(), QString())); + + writer.writeEndElement(); // description + writer.writeEndElement(); // page + writer.writeEndElement(); // document + writer.writeEndElement(); // WebXML + writer.writeEndDocument(); + + out() << data; + endSubPage(); +} + +void WebXMLGenerator::generateIndexSections(QXmlStreamWriter &writer, Node *node) +{ + marker_ = CodeMarker::markerForFileName(node->location().filePath()); + auto qdocIndexFiles = QDocIndexFiles::qdocIndexFiles(); + if (qdocIndexFiles) { + qdocIndexFiles->generateIndexSections(writer, node, this); + // generateIndexSections does nothing for groups, so handle them explicitly + if (node->isGroup()) + qdocIndexFiles->generateIndexSection(writer, node, this); + } +} + +// Handles callbacks from QDocIndexFiles to add documentation to node +void WebXMLGenerator::append(QXmlStreamWriter &writer, Node *node) +{ + Q_ASSERT(marker_); + + writer.writeStartElement("description"); + if (Config::instance().get(CONFIG_LOCATIONINFO).asBool()) { + writer.writeAttribute("path", node->doc().location().filePath()); + writer.writeAttribute("line", QString::number(node->doc().location().lineNo())); + writer.writeAttribute("column", QString::number(node->doc().location().columnNo())); + } + + if (node->isTextPageNode()) + generateRelations(writer, node); + + if (node->isModule()) { + writer.writeStartElement("generatedlist"); + writer.writeAttribute("contents", "classesbymodule"); + auto *cnn = static_cast<CollectionNode *>(node); + + if (cnn->hasNamespaces()) { + writer.writeStartElement("section"); + writer.writeStartElement("heading"); + writer.writeAttribute("level", "1"); + writer.writeCharacters("Namespaces"); + writer.writeEndElement(); // heading + NodeMap namespaces{cnn->getMembers(Node::Namespace)}; + generateAnnotatedList(writer, node, namespaces); + writer.writeEndElement(); // section + } + if (cnn->hasClasses()) { + writer.writeStartElement("section"); + writer.writeStartElement("heading"); + writer.writeAttribute("level", "1"); + writer.writeCharacters("Classes"); + writer.writeEndElement(); // heading + NodeMap classes{cnn->getMembers([](const Node *n){ return n->isClassNode(); })}; + generateAnnotatedList(writer, node, classes); + writer.writeEndElement(); // section + } + writer.writeEndElement(); // generatedlist + } + + m_inLink = m_inSectionHeading = m_hasQuotingInformation = false; + + const Atom *atom = node->doc().body().firstAtom(); + while (atom) + atom = addAtomElements(writer, atom, node, marker_); + + QList<Text> alsoList = node->doc().alsoList(); + supplementAlsoList(node, alsoList); + + if (!alsoList.isEmpty()) { + writer.writeStartElement("see-also"); + for (const auto &item : alsoList) { + const auto *atom = item.firstAtom(); + while (atom) + atom = addAtomElements(writer, atom, node, marker_); + } + writer.writeEndElement(); // see-also + } + + if (node->isExample()) { + m_supplement = true; + generateRequiredLinks(node, marker_); + m_supplement = false; + } else if (node->isGroup()) { + auto *cn = static_cast<CollectionNode *>(node); + if (!cn->noAutoList()) + generateAnnotatedList(writer, node, cn->members()); + } + + writer.writeEndElement(); // description +} + +void WebXMLGenerator::generateDocumentation(Node *node) +{ + // Don't generate nodes that are already processed, or if they're not supposed to + // generate output, ie. external, index or images nodes. + if (!node->url().isNull() || node->isExternalPage() || node->isIndexNode()) + return; + + if (node->isInternal() && !m_showInternal) + return; + + if (node->parent()) { + if (node->isNamespace() || node->isClassNode() || node->isHeader()) + generateCppReferencePage(static_cast<Aggregate *>(node), nullptr); + else if (node->isCollectionNode()) { + if (node->wasSeen()) { + // see remarks in base class impl. + m_qdb->mergeCollections(static_cast<CollectionNode *>(node)); + generatePageNode(static_cast<PageNode *>(node), nullptr); + } + } else if (node->isTextPageNode()) + generatePageNode(static_cast<PageNode *>(node), nullptr); + // else if TODO: anything else? + } + + if (node->isAggregate()) { + auto *aggregate = static_cast<Aggregate *>(node); + for (auto c : aggregate->childNodes()) { + if ((c->isAggregate() || c->isTextPageNode() || c->isCollectionNode()) + && !c->isPrivate()) + generateDocumentation(c); + } + } +} + +const Atom *WebXMLGenerator::addAtomElements(QXmlStreamWriter &writer, const Atom *atom, + const Node *relative, CodeMarker *marker) +{ + bool keepQuoting = false; + + if (!atom) + return nullptr; + + switch (atom->type()) { + case Atom::AnnotatedList: { + const CollectionNode *cn = m_qdb->getCollectionNode(atom->string(), Node::Group); + if (cn) + generateAnnotatedList(writer, relative, cn->members()); + } break; + case Atom::AutoLink: { + const Node *node{nullptr}; + QString link{}; + + if (!m_inLink && !m_inSectionHeading) { + link = getAutoLink(atom, relative, &node, Node::API); + + if (!link.isEmpty() && node && node->isDeprecated() + && relative->parent() != node && !relative->isDeprecated()) { + link.clear(); + } + } + + startLink(writer, atom, node, link); + + writer.writeCharacters(atom->string()); + + if (m_inLink) { + writer.writeEndElement(); // link + m_inLink = false; + } + + break; + } + case Atom::BaseName: + break; + case Atom::BriefLeft: + + writer.writeStartElement("brief"); + switch (relative->nodeType()) { + case Node::Property: + writer.writeCharacters("This property"); + break; + case Node::Variable: + writer.writeCharacters("This variable"); + break; + default: + break; + } + if (relative->isProperty() || relative->isVariable()) { + QString str; + const Atom *a = atom->next(); + while (a != nullptr && a->type() != Atom::BriefRight) { + if (a->type() == Atom::String || a->type() == Atom::AutoLink) + str += a->string(); + a = a->next(); + } + str[0] = str[0].toLower(); + if (str.endsWith('.')) + str.chop(1); + + const QList<QStringView> words = QStringView{str}.split(' '); + if (!words.isEmpty()) { + QStringView first(words.at(0)); + if (!(first == u"contains" || first == u"specifies" || first == u"describes" + || first == u"defines" || first == u"holds" || first == u"determines")) + writer.writeCharacters(" holds "); + else + writer.writeCharacters(" "); + } + } + break; + + case Atom::BriefRight: + if (relative->isProperty() || relative->isVariable()) + writer.writeCharacters("."); + + writer.writeEndElement(); // brief + break; + + case Atom::C: + writer.writeStartElement("teletype"); + if (m_inLink) + writer.writeAttribute("type", "normal"); + else + writer.writeAttribute("type", "highlighted"); + + writer.writeCharacters(plainCode(atom->string())); + writer.writeEndElement(); // teletype + break; + + case Atom::Code: + if (!m_hasQuotingInformation) + writer.writeTextElement( + "code", trimmedTrailing(plainCode(atom->string()), QString(), QString())); + else + keepQuoting = true; + break; + + case Atom::CodeBad: + writer.writeTextElement("badcode", + trimmedTrailing(plainCode(atom->string()), QString(), QString())); + break; + + case Atom::CodeQuoteArgument: + if (m_quoting) { + if (quoteCommand == "dots") { + writer.writeAttribute("indent", atom->string()); + writer.writeCharacters("..."); + } else { + writer.writeCharacters(atom->string()); + } + writer.writeEndElement(); // code + keepQuoting = true; + } + break; + + case Atom::CodeQuoteCommand: + if (m_quoting) { + quoteCommand = atom->string(); + writer.writeStartElement(quoteCommand); + } + break; + + case Atom::ExampleFileLink: { + if (!m_inLink) { + QString link = linkForExampleFile(atom->string()); + if (!link.isEmpty()) + startLink(writer, atom, relative, link); + } + } break; + + case Atom::ExampleImageLink: { + if (!m_inLink) { + QString link = atom->string(); + if (!link.isEmpty()) + startLink(writer, atom, nullptr, "images/used-in-examples/" + link); + } + } break; + + case Atom::FootnoteLeft: + writer.writeStartElement("footnote"); + break; + + case Atom::FootnoteRight: + writer.writeEndElement(); // footnote + break; + + case Atom::FormatEndif: + writer.writeEndElement(); // raw + break; + case Atom::FormatIf: + writer.writeStartElement("raw"); + writer.writeAttribute("format", atom->string()); + break; + case Atom::FormattingLeft: { + if (atom->string() == ATOM_FORMATTING_BOLD) + writer.writeStartElement("bold"); + else if (atom->string() == ATOM_FORMATTING_ITALIC) + writer.writeStartElement("italic"); + else if (atom->string() == ATOM_FORMATTING_UNDERLINE) + writer.writeStartElement("underline"); + else if (atom->string() == ATOM_FORMATTING_SUBSCRIPT) + writer.writeStartElement("subscript"); + else if (atom->string() == ATOM_FORMATTING_SUPERSCRIPT) + writer.writeStartElement("superscript"); + else if (atom->string() == ATOM_FORMATTING_TELETYPE) + writer.writeStartElement("teletype"); + else if (atom->string() == ATOM_FORMATTING_PARAMETER) + writer.writeStartElement("argument"); + else if (atom->string() == ATOM_FORMATTING_INDEX) + writer.writeStartElement("index"); + } break; + + case Atom::FormattingRight: { + if (atom->string() == ATOM_FORMATTING_BOLD) + writer.writeEndElement(); + else if (atom->string() == ATOM_FORMATTING_ITALIC) + writer.writeEndElement(); + else if (atom->string() == ATOM_FORMATTING_UNDERLINE) + writer.writeEndElement(); + else if (atom->string() == ATOM_FORMATTING_SUBSCRIPT) + writer.writeEndElement(); + else if (atom->string() == ATOM_FORMATTING_SUPERSCRIPT) + writer.writeEndElement(); + else if (atom->string() == ATOM_FORMATTING_TELETYPE) + writer.writeEndElement(); + else if (atom->string() == ATOM_FORMATTING_PARAMETER) + writer.writeEndElement(); + else if (atom->string() == ATOM_FORMATTING_INDEX) + writer.writeEndElement(); + else if (atom->string() == ATOM_FORMATTING_TRADEMARK && appendTrademark(atom)) + writer.writeCharacters(QChar(0x2122)); // 'TM' symbol + } + if (m_inLink) { + writer.writeEndElement(); // link + m_inLink = false; + } + break; + + case Atom::GeneratedList: + writer.writeStartElement("generatedlist"); + writer.writeAttribute("contents", atom->string()); + writer.writeEndElement(); + break; + + // TODO: The other generators treat inlineimage and image + // simultaneously as the diffirences aren't big. It should be + // possible to do the same for webxmlgenerator instead of + // repeating the code. + + // TODO: [generator-insufficient-structural-abstraction] + case Atom::Image: { + auto maybe_resolved_file{file_resolver.resolve(atom->string())}; + if (!maybe_resolved_file) { + // TODO: [uncentralized-admonition][failed-resolve-file] + relative->location().warning(QStringLiteral("Missing image: %1").arg(atom->string())); + } else { + ResolvedFile file{*maybe_resolved_file}; + QString file_name{QFileInfo{file.get_path()}.fileName()}; + + // TODO: [uncentralized-output-directory-structure] + Config::copyFile(relative->doc().location(), file.get_path(), file_name, outputDir() + QLatin1String("/images")); + + writer.writeStartElement("image"); + // TODO: [uncentralized-output-directory-structure] + writer.writeAttribute("href", "images/" + file_name); + writer.writeEndElement(); + // TODO: [uncentralized-output-directory-structure] + setImageFileName(relative, "images/" + file_name); + } + break; + } + // TODO: [generator-insufficient-structural-abstraction] + case Atom::InlineImage: { + auto maybe_resolved_file{file_resolver.resolve(atom->string())}; + if (!maybe_resolved_file) { + // TODO: [uncentralized-admonition][failed-resolve-file] + relative->location().warning(QStringLiteral("Missing image: %1").arg(atom->string())); + } else { + ResolvedFile file{*maybe_resolved_file}; + QString file_name{QFileInfo{file.get_path()}.fileName()}; + + // TODO: [uncentralized-output-directory-structure] + Config::copyFile(relative->doc().location(), file.get_path(), file_name, outputDir() + QLatin1String("/images")); + + writer.writeStartElement("inlineimage"); + // TODO: [uncentralized-output-directory-structure] + writer.writeAttribute("href", "images/" + file_name); + writer.writeEndElement(); + // TODO: [uncentralized-output-directory-structure] + setImageFileName(relative, "images/" + file_name); + } + break; + } + case Atom::ImageText: + break; + + case Atom::ImportantLeft: + writer.writeStartElement("para"); + writer.writeTextElement("bold", "Important:"); + writer.writeCharacters(" "); + break; + + case Atom::LegaleseLeft: + writer.writeStartElement("legalese"); + break; + + case Atom::LegaleseRight: + writer.writeEndElement(); // legalese + break; + + case Atom::Link: + case Atom::LinkNode: + if (!m_inLink) { + const Node *node = nullptr; + QString link = getLink(atom, relative, &node); + if (!link.isEmpty()) + startLink(writer, atom, node, link); + } + break; + + case Atom::ListLeft: + writer.writeStartElement("list"); + + if (atom->string() == ATOM_LIST_BULLET) + writer.writeAttribute("type", "bullet"); + else if (atom->string() == ATOM_LIST_TAG) + writer.writeAttribute("type", "definition"); + else if (atom->string() == ATOM_LIST_VALUE) { + if (relative->isEnumType()) + writer.writeAttribute("type", "enum"); + else + writer.writeAttribute("type", "definition"); + } else { + writer.writeAttribute("type", "ordered"); + if (atom->string() == ATOM_LIST_UPPERALPHA) + writer.writeAttribute("start", "A"); + else if (atom->string() == ATOM_LIST_LOWERALPHA) + writer.writeAttribute("start", "a"); + else if (atom->string() == ATOM_LIST_UPPERROMAN) + writer.writeAttribute("start", "I"); + else if (atom->string() == ATOM_LIST_LOWERROMAN) + writer.writeAttribute("start", "i"); + else // (atom->string() == ATOM_LIST_NUMERIC) + writer.writeAttribute("start", "1"); + } + break; + + case Atom::ListItemNumber: + break; + case Atom::ListTagLeft: { + writer.writeStartElement("definition"); + + writer.writeTextElement( + "term", plainCode(marker->markedUpEnumValue(atom->next()->string(), relative))); + } break; + + case Atom::ListTagRight: + writer.writeEndElement(); // definition + break; + + case Atom::ListItemLeft: + writer.writeStartElement("item"); + break; + + case Atom::ListItemRight: + writer.writeEndElement(); // item + break; + + case Atom::ListRight: + writer.writeEndElement(); // list + break; + + case Atom::NoteLeft: + writer.writeStartElement("para"); + writer.writeTextElement("bold", "Note:"); + writer.writeCharacters(" "); + break; + + // End admonition elements + case Atom::ImportantRight: + case Atom::NoteRight: + case Atom::WarningRight: + writer.writeEndElement(); // para + break; + + case Atom::Nop: + break; + + case Atom::CaptionLeft: + case Atom::ParaLeft: + writer.writeStartElement("para"); + break; + + case Atom::CaptionRight: + case Atom::ParaRight: + writer.writeEndElement(); // para + break; + + case Atom::QuotationLeft: + writer.writeStartElement("quote"); + break; + + case Atom::QuotationRight: + writer.writeEndElement(); // quote + break; + + case Atom::RawString: + writer.writeCharacters(atom->string()); + break; + + case Atom::SectionLeft: + writer.writeStartElement("section"); + writer.writeAttribute("id", + Utilities::asAsciiPrintable(Text::sectionHeading(atom).toString())); + break; + + case Atom::SectionRight: + writer.writeEndElement(); // section + break; + + case Atom::SectionHeadingLeft: { + writer.writeStartElement("heading"); + int unit = atom->string().toInt(); // + hOffset(relative) + writer.writeAttribute("level", QString::number(unit)); + m_inSectionHeading = true; + } break; + + case Atom::SectionHeadingRight: + writer.writeEndElement(); // heading + m_inSectionHeading = false; + break; + + case Atom::SidebarLeft: + case Atom::SidebarRight: + break; + + case Atom::SnippetCommand: + if (m_quoting) { + writer.writeStartElement(atom->string()); + } + break; + + case Atom::SnippetIdentifier: + if (m_quoting) { + writer.writeAttribute("identifier", atom->string()); + writer.writeEndElement(); + keepQuoting = true; + } + break; + + case Atom::SnippetLocation: + if (m_quoting) { + const QString &location = atom->string(); + writer.writeAttribute("location", location); + auto maybe_resolved_file{file_resolver.resolve(location)}; + // const QString resolved = Doc::resolveFile(Location(), location); + if (maybe_resolved_file) + writer.writeAttribute("path", (*maybe_resolved_file).get_path()); + else { + // TODO: [uncetnralized-admonition][failed-resolve-file] + QString details = std::transform_reduce( + file_resolver.get_search_directories().cbegin(), + file_resolver.get_search_directories().cend(), + u"Searched directories:"_s, + std::plus(), + [](const DirectoryPath &directory_path) -> QString { return u' ' + directory_path.value(); } + ); + + relative->location().warning(u"Cannot find file to quote from: %1"_s.arg(location), details); + } + } + break; + + case Atom::String: + writer.writeCharacters(atom->string()); + break; + case Atom::TableLeft: + writer.writeStartElement("table"); + if (atom->string().contains("%")) + writer.writeAttribute("width", atom->string()); + break; + + case Atom::TableRight: + writer.writeEndElement(); // table + break; + + case Atom::TableHeaderLeft: + writer.writeStartElement("header"); + break; + + case Atom::TableHeaderRight: + writer.writeEndElement(); // header + break; + + case Atom::TableRowLeft: + writer.writeStartElement("row"); + break; + + case Atom::TableRowRight: + writer.writeEndElement(); // row + break; + + case Atom::TableItemLeft: { + writer.writeStartElement("item"); + QStringList spans = atom->string().split(","); + if (spans.size() == 2) { + if (spans.at(0) != "1") + writer.writeAttribute("colspan", spans.at(0).trimmed()); + if (spans.at(1) != "1") + writer.writeAttribute("rowspan", spans.at(1).trimmed()); + } + } break; + case Atom::TableItemRight: + writer.writeEndElement(); // item + break; + + case Atom::Target: + writer.writeStartElement("target"); + writer.writeAttribute("name", Utilities::asAsciiPrintable(atom->string())); + writer.writeEndElement(); + break; + + case Atom::WarningLeft: + writer.writeStartElement("para"); + writer.writeTextElement("bold", "Warning:"); + writer.writeCharacters(" "); + break; + + case Atom::UnhandledFormat: + case Atom::UnknownCommand: + writer.writeCharacters(atom->typeString()); + break; + default: + break; + } + + m_hasQuotingInformation = keepQuoting; + return atom->next(); +} + +void WebXMLGenerator::startLink(QXmlStreamWriter &writer, const Atom *atom, const Node *node, + const QString &link) +{ + QString fullName = link; + if (node) + fullName = node->fullName(); + if (!fullName.isEmpty() && !link.isEmpty()) { + writer.writeStartElement("link"); + if (atom && !atom->string().isEmpty()) + writer.writeAttribute("raw", atom->string()); + else + writer.writeAttribute("raw", fullName); + writer.writeAttribute("href", link); + writer.writeAttribute("type", targetType(node)); + if (node) { + switch (node->nodeType()) { + case Node::Enum: + writer.writeAttribute("enum", fullName); + break; + case Node::Example: { + const auto *en = static_cast<const ExampleNode *>(node); + const QString fileTitle = atom ? exampleFileTitle(en, atom->string()) : QString(); + if (!fileTitle.isEmpty()) { + writer.writeAttribute("page", fileTitle); + break; + } + } + Q_FALLTHROUGH(); + case Node::Page: + writer.writeAttribute("page", fullName); + break; + case Node::Property: { + const auto *propertyNode = static_cast<const PropertyNode *>(node); + if (!propertyNode->getters().empty()) + writer.writeAttribute("getter", propertyNode->getters().at(0)->fullName()); + } break; + default: + break; + } + } + m_inLink = true; + } +} + +void WebXMLGenerator::endLink(QXmlStreamWriter &writer) +{ + if (m_inLink) { + writer.writeEndElement(); // link + m_inLink = false; + } +} + +void WebXMLGenerator::generateRelations(QXmlStreamWriter &writer, const Node *node) +{ + if (node && !node->links().empty()) { + std::pair<QString, QString> anchorPair; + const Node *linkNode; + + for (auto it = node->links().cbegin(); it != node->links().cend(); ++it) { + + linkNode = m_qdb->findNodeForTarget(it.value().first, node); + + if (!linkNode) + linkNode = node; + + if (linkNode == node) + anchorPair = it.value(); + else + anchorPair = anchorForNode(linkNode); + + writer.writeStartElement("relation"); + writer.writeAttribute("href", anchorPair.first); + writer.writeAttribute("type", targetType(linkNode)); + + switch (it.key()) { + case Node::StartLink: + writer.writeAttribute("meta", "start"); + break; + case Node::NextLink: + writer.writeAttribute("meta", "next"); + break; + case Node::PreviousLink: + writer.writeAttribute("meta", "previous"); + break; + case Node::ContentsLink: + writer.writeAttribute("meta", "contents"); + break; + default: + writer.writeAttribute("meta", ""); + } + writer.writeAttribute("description", anchorPair.second); + writer.writeEndElement(); // link + } + } +} + +void WebXMLGenerator::generateAnnotatedList(QXmlStreamWriter &writer, const Node *relative, + const NodeMap &nodeMap) +{ + generateAnnotatedList(writer, relative, nodeMap.values()); +} + +void WebXMLGenerator::generateAnnotatedList(QXmlStreamWriter &writer, const Node *relative, + const NodeList &nodeList) +{ + writer.writeStartElement("table"); + writer.writeAttribute("width", "100%"); + + for (const auto *node : nodeList) { + writer.writeStartElement("row"); + writer.writeStartElement("item"); + writer.writeStartElement("para"); + const QString link = linkForNode(node, relative); + startLink(writer, node->doc().body().firstAtom(), node, link); + endLink(writer); + writer.writeEndElement(); // para + writer.writeEndElement(); // item + + writer.writeStartElement("item"); + writer.writeStartElement("para"); + writer.writeCharacters(node->doc().briefText().toString()); + writer.writeEndElement(); // para + writer.writeEndElement(); // item + writer.writeEndElement(); // row + } + writer.writeEndElement(); // table +} + +QString WebXMLGenerator::fileBase(const Node *node) const +{ + return Generator::fileBase(node); +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/webxmlgenerator.h b/src/qdoc/qdoc/src/qdoc/webxmlgenerator.h new file mode 100644 index 000000000..7065670b2 --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/webxmlgenerator.h @@ -0,0 +1,60 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef WEBXMLGENERATOR_H +#define WEBXMLGENERATOR_H + +#include "codemarker.h" +#include "htmlgenerator.h" +#include "qdocindexfiles.h" + +#include <QtCore/qscopedpointer.h> +#include <QtCore/qxmlstream.h> + +QT_BEGIN_NAMESPACE + +class Aggregate; + +class WebXMLGenerator : public HtmlGenerator, public IndexSectionWriter +{ +public: + WebXMLGenerator(FileResolver& file_resolver); + + void initializeGenerator() override; + void terminateGenerator() override; + QString format() override; + // from IndexSectionWriter + void append(QXmlStreamWriter &writer, Node *node) override; + +protected: + qsizetype generateAtom(const Atom *atom, const Node *relative, CodeMarker *marker) override; + void generateCppReferencePage(Aggregate *aggregate, CodeMarker *marker) override; + void generatePageNode(PageNode *pn, CodeMarker *marker) override; + void generateDocumentation(Node *node) override; + void generateExampleFilePage(const Node *en, ResolvedFile file, CodeMarker *marker = nullptr) override; + [[nodiscard]] QString fileExtension() const override; + + virtual const Atom *addAtomElements(QXmlStreamWriter &writer, const Atom *atom, + const Node *relative, CodeMarker *marker); + virtual void generateIndexSections(QXmlStreamWriter &writer, Node *node); + +private: + void generateAnnotatedList(QXmlStreamWriter &writer, const Node *relative, + const NodeMap &nodeMap); + void generateAnnotatedList(QXmlStreamWriter &writer, const Node *relative, + const NodeList &nodeList); + void generateRelations(QXmlStreamWriter &writer, const Node *node); + void startLink(QXmlStreamWriter &writer, const Atom *atom, const Node *node, + const QString &link); + void endLink(QXmlStreamWriter &writer); + QString fileBase(const Node *node) const override; + + bool m_hasQuotingInformation { false }; + QString quoteCommand {}; + QScopedPointer<QXmlStreamWriter> currentWriter {}; + bool m_supplement { false }; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/qdoc/qdoc/src/qdoc/xmlgenerator.cpp b/src/qdoc/qdoc/src/qdoc/xmlgenerator.cpp new file mode 100644 index 000000000..ffad5259d --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/xmlgenerator.cpp @@ -0,0 +1,487 @@ +// Copyright (C) 2019 Thibaut Cuvelier +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "xmlgenerator.h" + +#include "enumnode.h" +#include "examplenode.h" +#include "functionnode.h" +#include "qdocdatabase.h" +#include "typedefnode.h" + +using namespace Qt::Literals::StringLiterals; + +QT_BEGIN_NAMESPACE + +const QRegularExpression XmlGenerator::m_funcLeftParen(QStringLiteral("^\\S+(\\(.*\\))")); + +XmlGenerator::XmlGenerator(FileResolver& file_resolver) : Generator(file_resolver) {} + +/*! + Do not display \brief for QML types, document and collection nodes + */ +bool XmlGenerator::hasBrief(const Node *node) +{ + return !(node->isQmlType() || node->isPageNode() || node->isCollectionNode()); +} + +/*! + Determines whether the list atom should be shown with three columns + (constant-value-description). + */ +bool XmlGenerator::isThreeColumnEnumValueTable(const Atom *atom) +{ + while (atom && !(atom->type() == Atom::ListRight && atom->string() == ATOM_LIST_VALUE)) { + if (atom->type() == Atom::ListItemLeft && !matchAhead(atom, Atom::ListItemRight)) + return true; + atom = atom->next(); + } + return false; +} + +/*! + Determines whether the list atom should be shown with just one column (value). + */ +bool XmlGenerator::isOneColumnValueTable(const Atom *atom) +{ + if (atom->type() != Atom::ListLeft || atom->string() != ATOM_LIST_VALUE) + return false; + + while (atom && atom->type() != Atom::ListTagRight) + atom = atom->next(); + + if (atom) { + if (!matchAhead(atom, Atom::ListItemLeft)) + return false; + if (!atom->next()) + return false; + return matchAhead(atom->next(), Atom::ListItemRight); + } + return false; +} + +/*! + Header offset depending on the type of the node + */ +int XmlGenerator::hOffset(const Node *node) +{ + switch (node->nodeType()) { + case Node::Namespace: + case Node::Class: + case Node::Struct: + case Node::Union: + case Node::Module: + return 2; + case Node::QmlModule: + case Node::QmlValueType: + case Node::QmlType: + case Node::Page: + case Node::Group: + return 1; + case Node::Enum: + case Node::TypeAlias: + case Node::Typedef: + case Node::Function: + case Node::Property: + default: + return 3; + } +} + +/*! + Rewrites the brief of this node depending on its first word. + Only for properties and variables (does nothing otherwise). + */ +void XmlGenerator::rewritePropertyBrief(const Atom *atom, const Node *relative) +{ + if (relative->nodeType() != Node::Property && relative->nodeType() != Node::Variable) + return; + atom = atom->next(); + if (!atom || atom->type() != Atom::String) + return; + + const QString firstWord = + atom->string().toLower().section(' ', 0, 0, QString::SectionSkipEmpty); + const QStringList words{ "the", "a", "an", "whether", "which" }; + if (words.contains(firstWord)) { + QString str = QLatin1String("This ") + + QLatin1String(relative->nodeType() == Node::Property ? "property" : "variable") + + QLatin1String(" holds ") + atom->string().left(1).toLower() + + atom->string().mid(1); + const_cast<Atom *>(atom)->setString(str); + } +} + +/*! + Returns the type of this atom as an enumeration. + */ +Node::NodeType XmlGenerator::typeFromString(const Atom *atom) +{ + const auto &name = atom->string(); + if (name.startsWith(QLatin1String("qml"))) + return Node::QmlModule; + else if (name.startsWith(QLatin1String("groups"))) + return Node::Group; + else + return Node::Module; +} + +/*! + For images shown in examples, set the image file to the one it + will have once the documentation is generated. + */ +void XmlGenerator::setImageFileName(const Node *relative, const QString &fileName) +{ + if (relative->isExample()) { + const auto cen = static_cast<const ExampleNode *>(relative); + if (cen->imageFileName().isEmpty()) { + auto *en = const_cast<ExampleNode *>(cen); + en->setImageFileName(fileName); + } + } +} + +/*! + Handles the differences in lists between list tags and since tags, and + returns the content of the list entry \a atom (first member of the pair). + It also returns the number of items to skip ahead (second member of the pair). + */ +std::pair<QString, int> XmlGenerator::getAtomListValue(const Atom *atom) +{ + const Atom *lookAhead = atom->next(); + if (!lookAhead) + return std::pair<QString, int>(QString(), 1); + + QString t = lookAhead->string(); + lookAhead = lookAhead->next(); + if (!lookAhead || lookAhead->type() != Atom::ListTagRight) + return std::pair<QString, int>(QString(), 1); + + lookAhead = lookAhead->next(); + int skipAhead; + if (lookAhead && lookAhead->type() == Atom::SinceTagLeft) { + lookAhead = lookAhead->next(); + Q_ASSERT(lookAhead && lookAhead->type() == Atom::String); + t += QLatin1String(" (since "); + if (lookAhead->string().at(0).isDigit()) + t += QLatin1String("Qt "); + t += lookAhead->string() + QLatin1String(")"); + skipAhead = 4; + } else { + skipAhead = 1; + } + return std::pair<QString, int>(t, skipAhead); +} + +/*! + Parses the table attributes from the given \a atom. + This method returns a pair containing the width (%) and + the attribute for this table (either "generic" or + "borderless"). + */ +std::pair<QString, QString> XmlGenerator::getTableWidthAttr(const Atom *atom) +{ + QString p0, p1; + QString attr = "generic"; + QString width; + if (atom->count() > 0) { + p0 = atom->string(0); + if (atom->count() > 1) + p1 = atom->string(1); + } + if (!p0.isEmpty()) { + if (p0 == QLatin1String("borderless")) + attr = p0; + else if (p0.contains(QLatin1Char('%'))) + width = p0; + } + if (!p1.isEmpty()) { + if (p1 == QLatin1String("borderless")) + attr = p1; + else if (p1.contains(QLatin1Char('%'))) + width = p1; + } + + // Many times, in the documentation, there is a space before the % sign: + // this breaks the parsing logic above. + if (width == QLatin1String("%")) { + // The percentage is typically stored in p0, parse it as an int. + bool ok = false; + int widthPercentage = p0.toInt(&ok); + if (ok) { + width = QString::number(widthPercentage) + "%"; + } else { + width = {}; + } + } + + return {width, attr}; +} + +/*! + Registers an anchor reference and returns a unique + and cleaned copy of the reference (the one that should be + used in the output). + To ensure unicity throughout the document, this method + uses the \a refMap cache. + */ +QString XmlGenerator::registerRef(const QString &ref, bool xmlCompliant) +{ + QString cleanRef = Generator::cleanRef(ref, xmlCompliant); + + for (;;) { + QString &prevRef = refMap[cleanRef.toLower()]; + if (prevRef.isEmpty()) { + // This reference has never been met before for this document: register it. + prevRef = ref; + break; + } else if (prevRef == ref) { + // This exact same reference was already found. This case typically occurs within refForNode. + break; + } + cleanRef += QLatin1Char('x'); + } + return cleanRef; +} + +/*! + Generates a clean and unique reference for the given \a node. + This reference may depend on the type of the node (typedef, + QML signal, etc.) + */ +QString XmlGenerator::refForNode(const Node *node) +{ + QString ref; + switch (node->nodeType()) { + case Node::Enum: + ref = node->name() + "-enum"; + break; + case Node::Typedef: { + const auto *tdf = static_cast<const TypedefNode *>(node); + if (tdf->associatedEnum()) + return refForNode(tdf->associatedEnum()); + } Q_FALLTHROUGH(); + case Node::TypeAlias: + ref = node->name() + "-typedef"; + break; + case Node::Function: { + const auto fn = static_cast<const FunctionNode *>(node); + switch (fn->metaness()) { + case FunctionNode::QmlSignal: + ref = fn->name() + "-signal"; + break; + case FunctionNode::QmlSignalHandler: + ref = fn->name() + "-signal-handler"; + break; + case FunctionNode::QmlMethod: + ref = fn->name() + "-method"; + if (fn->overloadNumber() != 0) + ref += QLatin1Char('-') + QString::number(fn->overloadNumber()); + break; + default: + if (fn->hasOneAssociatedProperty() && fn->doc().isEmpty()) { + return refForNode(fn->associatedProperties()[0]); + } else { + ref = fn->name(); + if (fn->overloadNumber() != 0) + ref += QLatin1Char('-') + QString::number(fn->overloadNumber()); + } + break; + } + } break; + case Node::SharedComment: { + if (!node->isPropertyGroup()) + break; + } Q_FALLTHROUGH(); + case Node::QmlProperty: + if (node->isAttached()) + ref = node->name() + "-attached-prop"; + else + ref = node->name() + "-prop"; + break; + case Node::Property: + ref = node->name() + "-prop"; + break; + case Node::Variable: + ref = node->name() + "-var"; + break; + default: + break; + } + return registerRef(ref); +} + +/*! + Construct the link string for the \a node and return it. + The \a relative node is used to decide whether the link + we are generating is in the same file as the target. + Note the relative node can be 0, which pretty much + guarantees that the link and the target aren't in the + same file. + */ +QString XmlGenerator::linkForNode(const Node *node, const Node *relative) +{ + if (node == nullptr) + return QString(); + if (!node->url().isNull()) + return node->url(); + if (fileBase(node).isEmpty()) + return QString(); + if (node->isPrivate()) + return QString(); + + QString fn = fileName(node); + if (node->parent() && node->parent()->isQmlType() && node->parent()->isAbstract()) { + if (Generator::qmlTypeContext()) { + if (Generator::qmlTypeContext()->inherits(node->parent())) { + fn = fileName(Generator::qmlTypeContext()); + } else if (node->parent()->isInternal() && !noLinkErrors()) { + node->doc().location().warning( + QStringLiteral("Cannot link to property in internal type '%1'") + .arg(node->parent()->name())); + return QString(); + } + } + } + + QString link = fn; + + if (!node->isPageNode() || node->isPropertyGroup()) { + QString ref = refForNode(node); + if (relative && fn == fileName(relative) && ref == refForNode(relative)) + return QString(); + + link += QLatin1Char('#'); + link += ref; + } + + /* + If the output is going to subdirectories, the + two nodes have different output directories if 'node' + was read from index. + */ + if (relative && (node != relative)) { + if (useOutputSubdirs() && !node->isExternalPage() && node->isIndexNode()) + link.prepend("../%1/"_L1.arg(node->tree()->physicalModuleName())); + } + return link; +} + +/*! + This function is called for links, i.e. for words that + are marked with the qdoc link command. For autolinks + that are not marked with the qdoc link command, the + getAutoLink() function is called + + It returns the string for a link found by using the data + in the \a atom to search the database. It also sets \a node + to point to the target node for that link. \a relative points + to the node holding the qdoc comment where the link command + was found. + */ +QString XmlGenerator::getLink(const Atom *atom, const Node *relative, const Node **node) +{ + const QString &t = atom->string(); + + if (t.isEmpty()) + return t; + + if (t.at(0) == QChar('h')) { + if (t.startsWith("http:") || t.startsWith("https:")) + return t; + } else if (t.at(0) == QChar('f')) { + if (t.startsWith("file:") || t.startsWith("ftp:")) + return t; + } else if (t.at(0) == QChar('m')) { + if (t.startsWith("mailto:")) + return t; + } + return getAutoLink(atom, relative, node); +} + +/*! + This function is called for autolinks, i.e. for words that + are not marked with the qdoc link command that qdoc has + reason to believe should be links. + + It returns the string for a link found by using the data + in the \a atom to search the database. It also sets \a node + to point to the target node for that link. \a relative points + to the node holding the qdoc comment where the link command + was found. + */ +QString XmlGenerator::getAutoLink(const Atom *atom, const Node *relative, const Node **node, + Node::Genus genus) +{ + QString ref; + + *node = m_qdb->findNodeForAtom(atom, relative, ref, genus); + if (!(*node)) + return QString(); + + QString link = (*node)->url(); + if (link.isNull()) { + link = linkForNode(*node, relative); + } else if (link.isEmpty()) { + return link; // Explicit empty url (node is ignored as a link target) + } + if (!ref.isEmpty()) { + qsizetype hashtag = link.lastIndexOf(QChar('#')); + if (hashtag != -1) + link.truncate(hashtag); + link += QLatin1Char('#') + ref; + } + return link; +} + +std::pair<QString, QString> XmlGenerator::anchorForNode(const Node *node) +{ + std::pair<QString, QString> anchorPair; + + anchorPair.first = Generator::fileName(node); + if (node->isTextPageNode()) + anchorPair.second = node->title(); + + return anchorPair; +} + +/*! + Returns a string describing the \a node type. + */ +QString XmlGenerator::targetType(const Node *node) +{ + if (!node) + return QStringLiteral("external"); + + switch (node->nodeType()) { + case Node::Namespace: + return QStringLiteral("namespace"); + case Node::Class: + case Node::Struct: + case Node::Union: + return QStringLiteral("class"); + case Node::Page: + case Node::Example: + return QStringLiteral("page"); + case Node::Enum: + return QStringLiteral("enum"); + case Node::TypeAlias: + return QStringLiteral("alias"); + case Node::Typedef: + return QStringLiteral("typedef"); + case Node::Property: + return QStringLiteral("property"); + case Node::Function: + return QStringLiteral("function"); + case Node::Variable: + return QStringLiteral("variable"); + case Node::Module: + return QStringLiteral("module"); + default: + break; + } + return QString(); +} + +QT_END_NAMESPACE diff --git a/src/qdoc/qdoc/src/qdoc/xmlgenerator.h b/src/qdoc/qdoc/src/qdoc/xmlgenerator.h new file mode 100644 index 000000000..5f7ba67fd --- /dev/null +++ b/src/qdoc/qdoc/src/qdoc/xmlgenerator.h @@ -0,0 +1,54 @@ +// Copyright (C) 2019 Thibaut Cuvelier +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef XMLGENERATOR_H +#define XMLGENERATOR_H + +#include "node.h" +#include "generator.h" +#include "filesystem/fileresolver.h" + +#include <QtCore/qmap.h> +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +class XmlGenerator : public Generator +{ +public: + explicit XmlGenerator(FileResolver& file_resolver); + +protected: + QHash<QString, QString> refMap; + + static bool hasBrief(const Node *node); + static bool isThreeColumnEnumValueTable(const Atom *atom); + static bool isOneColumnValueTable(const Atom *atom); + static int hOffset(const Node *node); + + static void rewritePropertyBrief(const Atom *atom, const Node *relative); + static Node::NodeType typeFromString(const Atom *atom); + static void setImageFileName(const Node *relative, const QString &fileName); + static std::pair<QString, int> getAtomListValue(const Atom *atom); + static std::pair<QString, QString> getTableWidthAttr(const Atom *atom); + + QString registerRef(const QString &ref, bool xmlCompliant = false); + QString refForNode(const Node *node); + QString linkForNode(const Node *node, const Node *relative); + QString getLink(const Atom *atom, const Node *relative, const Node **node); + QString getAutoLink(const Atom *atom, const Node *relative, const Node **node, + Node::Genus = Node::DontCare); + + std::pair<QString, QString> anchorForNode(const Node *node); + + static QString targetType(const Node *node); + +protected: + static const QRegularExpression m_funcLeftParen; + const Node *m_linkNode { nullptr }; +}; + +QT_END_NAMESPACE + +#endif // XMLGENERATOR_H |