1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include "cppcodeparser.h"
5
6#include "access.h"
7#include "classnode.h"
8#include "collectionnode.h"
9#include "config.h"
10#include "examplenode.h"
11#include "externalpagenode.h"
12#include "functionnode.h"
13#include "generator.h"
14#include "headernode.h"
15#include "namespacenode.h"
16#include "qdocdatabase.h"
17#include "qmltypenode.h"
18#include "qmlpropertynode.h"
19#include "sharedcommentnode.h"
20#include "utilities.h"
21
22#include <QtCore/qdebug.h>
23#include <QtCore/qmap.h>
24
25#include <algorithm>
26
27using namespace Qt::Literals::StringLiterals;
28
29QT_BEGIN_NAMESPACE
30
31/* qmake ignore Q_OBJECT */
32
33QSet<QString> CppCodeParser::m_excludeDirs;
34QSet<QString> CppCodeParser::m_excludeFiles;
35
36/*
37 All these can appear in a C++ namespace. Don't add
38 anything that can't be in a C++ namespace.
39 */
40static const QMap<QString, Node::NodeType> s_nodeTypeMap{
41 { COMMAND_NAMESPACE, Node::Namespace }, { COMMAND_NAMESPACE, Node::Namespace },
42 { COMMAND_CLASS, Node::Class }, { COMMAND_STRUCT, Node::Struct },
43 { COMMAND_UNION, Node::Union }, { COMMAND_ENUM, Node::Enum },
44 { COMMAND_TYPEALIAS, Node::TypeAlias }, { COMMAND_TYPEDEF, Node::Typedef },
45 { COMMAND_PROPERTY, Node::Property }, { COMMAND_VARIABLE, Node::Variable }
46};
47
48typedef bool (Node::*NodeTypeTestFunc)() const;
49static const QMap<QString, NodeTypeTestFunc> s_nodeTypeTestFuncMap{
50 { COMMAND_NAMESPACE, &Node::isNamespace }, { COMMAND_CLASS, &Node::isClassNode },
51 { COMMAND_STRUCT, &Node::isStruct }, { COMMAND_UNION, &Node::isUnion },
52 { COMMAND_ENUM, &Node::isEnumType }, { COMMAND_TYPEALIAS, &Node::isTypeAlias },
53 { COMMAND_TYPEDEF, &Node::isTypedef }, { COMMAND_PROPERTY, &Node::isProperty },
54 { COMMAND_VARIABLE, &Node::isVariable },
55};
56
57CppCodeParser::CppCodeParser()
58{
59 Config &config = Config::instance();
60 QStringList exampleFilePatterns{config.get(CONFIG_EXAMPLES
61 + Config::dot
62 + CONFIG_FILEEXTENSIONS).asStringList()};
63
64 // Used for excluding dirs and files from the list of example files
65 const auto &excludeDirsList = config.getCanonicalPathList(CONFIG_EXCLUDEDIRS);
66 m_excludeDirs = QSet<QString>(excludeDirsList.cbegin(), excludeDirsList.cend());
67 const auto &excludeFilesList = config.getCanonicalPathList(CONFIG_EXCLUDEDIRS);
68 m_excludeFiles = QSet<QString>(excludeFilesList.cbegin(), excludeFilesList.cend());
69
70 if (!exampleFilePatterns.isEmpty())
71 m_exampleNameFilter = exampleFilePatterns.join(sep: ' ');
72 else
73 m_exampleNameFilter = "*.cpp *.h *.js *.xq *.svg *.xml *.ui";
74
75 QStringList exampleImagePatterns{config.get(CONFIG_EXAMPLES
76 + Config::dot
77 + CONFIG_IMAGEEXTENSIONS).asStringList()};
78
79 if (!exampleImagePatterns.isEmpty())
80 m_exampleImageFilter = exampleImagePatterns.join(sep: ' ');
81 else
82 m_exampleImageFilter = "*.png";
83
84 m_showLinkErrors = !config.get(CONFIG_NOLINKERRORS).asBool();
85}
86
87/*!
88 Clear the exclude directories and exclude files sets.
89 */
90CppCodeParser::~CppCodeParser()
91{
92 m_excludeDirs.clear();
93 m_excludeFiles.clear();
94}
95
96/*!
97 Process the topic \a command found in the \a doc with argument \a arg.
98 */
99Node *CppCodeParser::processTopicCommand(const Doc &doc, const QString &command,
100 const ArgPair &arg)
101{
102 QDocDatabase* database = QDocDatabase::qdocDB();
103
104 if (command == COMMAND_FN) {
105 Q_UNREACHABLE();
106 } else if (s_nodeTypeMap.contains(key: command)) {
107 /*
108 We should only get in here if the command refers to
109 something that can appear in a C++ namespace,
110 i.e. a class, another namespace, an enum, a typedef,
111 a property or a variable. I think these are handled
112 this way to allow the writer to refer to the entity
113 without including the namespace qualifier.
114 */
115 Node::NodeType type = s_nodeTypeMap[command];
116 QStringList words = arg.first.split(sep: QLatin1Char(' '));
117 QStringList path;
118 qsizetype idx = 0;
119 Node *node = nullptr;
120
121 if (type == Node::Variable && words.size() > 1)
122 idx = words.size() - 1;
123 path = words[idx].split(sep: "::");
124
125 node = database->findNodeInOpenNamespace(path, s_nodeTypeTestFuncMap[command]);
126 if (node == nullptr)
127 node = database->findNodeByNameAndType(path, isMatch: s_nodeTypeTestFuncMap[command]);
128 // Allow representing a type alias as a class
129 if (node == nullptr && command == COMMAND_CLASS) {
130 node = database->findNodeByNameAndType(path, isMatch: &Node::isTypeAlias);
131 if (node) {
132 auto access = node->access();
133 auto loc = node->location();
134 auto templateDecl = node->templateDecl();
135 node = new ClassNode(Node::Class, node->parent(), node->name());
136 node->setAccess(access);
137 node->setLocation(loc);
138 node->setTemplateDecl(templateDecl);
139 }
140 }
141 if (node == nullptr) {
142 if (CodeParser::isWorthWarningAbout(doc)) {
143 doc.location().warning(
144 QStringLiteral("Cannot find '%1' specified with '\\%2' in any header file")
145 .arg(args: arg.first, args: command));
146 }
147 } else if (node->isAggregate()) {
148 if (type == Node::Namespace) {
149 auto *ns = static_cast<NamespaceNode *>(node);
150 ns->markSeen();
151 ns->setWhereDocumented(ns->tree()->camelCaseModuleName());
152 }
153 /*
154 This treats a class as a namespace.
155 */
156 if ((type == Node::Class) || (type == Node::Namespace) || (type == Node::Struct)
157 || (type == Node::Union)) {
158 if (path.size() > 1) {
159 path.pop_back();
160 QString ns = path.join(sep: QLatin1String("::"));
161 database->insertOpenNamespace(path: ns);
162 }
163 }
164 }
165 return node;
166 } else if (command == COMMAND_EXAMPLE) {
167 if (Config::generateExamples) {
168 auto *en = new ExampleNode(database->primaryTreeRoot(), arg.first);
169 en->setLocation(doc.startLocation());
170 setExampleFileLists(en);
171 return en;
172 }
173 } else if (command == COMMAND_EXTERNALPAGE) {
174 auto *epn = new ExternalPageNode(database->primaryTreeRoot(), arg.first);
175 epn->setLocation(doc.startLocation());
176 return epn;
177 } else if (command == COMMAND_HEADERFILE) {
178 auto *hn = new HeaderNode(database->primaryTreeRoot(), arg.first);
179 hn->setLocation(doc.startLocation());
180 return hn;
181 } else if (command == COMMAND_GROUP) {
182 CollectionNode *cn = database->addGroup(name: arg.first);
183 cn->setLocation(doc.startLocation());
184 cn->markSeen();
185 return cn;
186 } else if (command == COMMAND_MODULE) {
187 CollectionNode *cn = database->addModule(name: arg.first);
188 cn->setLocation(doc.startLocation());
189 cn->markSeen();
190 return cn;
191 } else if (command == COMMAND_QMLMODULE) {
192 QStringList blankSplit = arg.first.split(sep: QLatin1Char(' '));
193 CollectionNode *cn = database->addQmlModule(name: blankSplit[0]);
194 cn->setLogicalModuleInfo(blankSplit);
195 cn->setLocation(doc.startLocation());
196 cn->markSeen();
197 return cn;
198 } else if (command == COMMAND_PAGE) {
199 auto *pn = new PageNode(database->primaryTreeRoot(), arg.first.split(sep: ' ').front());
200 pn->setLocation(doc.startLocation());
201 return pn;
202 } else if (command == COMMAND_QMLTYPE ||
203 command == COMMAND_QMLVALUETYPE ||
204 command == COMMAND_QMLBASICTYPE) {
205 QmlTypeNode *qcn = nullptr;
206 auto nodeType = (command == COMMAND_QMLTYPE) ? Node::QmlType : Node::QmlValueType;
207 Node *candidate = database->primaryTreeRoot()->findChildNode(name: arg.first, genus: Node::QML);
208 qcn = (candidate && candidate->nodeType() == nodeType) ?
209 static_cast<QmlTypeNode *>(candidate) :
210 new QmlTypeNode(database->primaryTreeRoot(), arg.first, nodeType);
211 qcn->setLocation(doc.startLocation());
212 return qcn;
213 } else if ((command == COMMAND_QMLSIGNAL) || (command == COMMAND_QMLMETHOD)
214 || (command == COMMAND_QMLATTACHEDSIGNAL) || (command == COMMAND_QMLATTACHEDMETHOD)) {
215 Q_UNREACHABLE();
216 }
217 return nullptr;
218}
219
220/*!
221 A QML property argument has the form...
222
223 <type> <QML-type>::<name>
224 <type> <QML-module>::<QML-type>::<name>
225
226 This function splits the argument into one of those
227 two forms. The three part form is the old form, which
228 was used before the creation of Qt Quick 2 and Qt
229 Components. A <QML-module> is the QML equivalent of a
230 C++ namespace. So this function splits \a arg on "::"
231 and stores the parts in \a type, \a module, \a qmlTypeName,
232 and \a name, and returns \c true. If any part other than
233 \a module is not found, a qdoc warning is emitted and
234 false is returned.
235
236 \note The two QML types \e{Component} and \e{QtObject}
237 never have a module qualifier.
238 */
239bool CppCodeParser::splitQmlPropertyArg(const QString &arg, QString &type, QString &module,
240 QString &qmlTypeName, QString &name,
241 const Location &location)
242{
243 QStringList blankSplit = arg.split(sep: QLatin1Char(' '));
244 if (blankSplit.size() > 1) {
245 type = blankSplit[0];
246 QStringList colonSplit(blankSplit[1].split(sep: "::"));
247 if (colonSplit.size() == 3) {
248 module = colonSplit[0];
249 qmlTypeName = colonSplit[1];
250 name = colonSplit[2];
251 return true;
252 }
253 if (colonSplit.size() == 2) {
254 module.clear();
255 qmlTypeName = colonSplit[0];
256 name = colonSplit[1];
257 return true;
258 }
259 location.warning(
260 QStringLiteral("Unrecognizable QML module/component qualifier for %1").arg(a: arg));
261 } else {
262 location.warning(QStringLiteral("Missing property type for %1").arg(a: arg));
263 }
264 return false;
265}
266
267/*!
268 */
269void CppCodeParser::processQmlProperties(const Doc &doc, NodeList &nodes, DocList &docs)
270{
271 const TopicList &topics = doc.topicsUsed();
272 if (topics.isEmpty())
273 return;
274
275 QString arg;
276 QString type;
277 QString group;
278 QString module;
279 QString property;
280 QString qmlTypeName;
281
282 Topic topic = topics.at(i: 0);
283 arg = topic.m_args;
284 if (splitQmlPropertyArg(arg, type, module, qmlTypeName, name&: property, location: doc.location())) {
285 qsizetype i = property.indexOf(c: '.');
286 if (i != -1)
287 group = property.left(n: i);
288 }
289
290 QDocDatabase* database = QDocDatabase::qdocDB();
291
292 NodeList sharedNodes;
293 QmlTypeNode *qmlType = database->findQmlType(qmid: module, name: qmlTypeName);
294 // Note: Constructing a QmlType node by default, as opposed to QmlValueType.
295 // This may lead to unexpected behavior if documenting \qmlvaluetype's properties
296 // before the type itself.
297 if (qmlType == nullptr)
298 qmlType = new QmlTypeNode(database->primaryTreeRoot(), qmlTypeName, Node::QmlType);
299
300 for (const auto &topicCommand : topics) {
301 QString cmd = topicCommand.m_topic;
302 arg = topicCommand.m_args;
303 if ((cmd == COMMAND_QMLPROPERTY) || (cmd == COMMAND_QMLATTACHEDPROPERTY)) {
304 bool attached = cmd.contains(s: QLatin1String("attached"));
305 if (splitQmlPropertyArg(arg, type, module, qmlTypeName, name&: property, location: doc.location())) {
306 if (qmlType != database->findQmlType(qmid: module, name: qmlTypeName)) {
307 doc.startLocation().warning(
308 QStringLiteral(
309 "All properties in a group must belong to the same type: '%1'")
310 .arg(a: arg));
311 continue;
312 }
313 QmlPropertyNode *existingProperty = qmlType->hasQmlProperty(property, attached);
314 if (existingProperty) {
315 processMetaCommands(doc, node: existingProperty);
316 if (!doc.body().isEmpty()) {
317 doc.startLocation().warning(
318 QStringLiteral("QML property documented multiple times: '%1'")
319 .arg(a: arg), QStringLiteral("also seen here: %1")
320 .arg(a: existingProperty->location().toString()));
321 }
322 continue;
323 }
324 auto *qpn = new QmlPropertyNode(qmlType, property, type, attached);
325 qpn->setLocation(doc.startLocation());
326 qpn->setGenus(Node::QML);
327 nodes.append(t: qpn);
328 docs.append(t: doc);
329 sharedNodes << qpn;
330 }
331 } else {
332 doc.startLocation().warning(
333 QStringLiteral("Command '\\%1'; not allowed with QML property commands")
334 .arg(a: cmd));
335 }
336 }
337
338 // Construct a SharedCommentNode (scn) if multiple topics generated
339 // valid nodes. Note that it's important to do this *after* constructing
340 // the topic nodes - which need to be written to index before the related
341 // scn.
342 if (sharedNodes.size() > 1) {
343 auto *scn = new SharedCommentNode(qmlType, sharedNodes.size(), group);
344 scn->setLocation(doc.startLocation());
345 nodes.append(t: scn);
346 docs.append(t: doc);
347 for (const auto n : sharedNodes)
348 scn->append(node: n);
349 scn->sort();
350 }
351}
352
353/*!
354 Process the metacommand \a command in the context of the
355 \a node associated with the topic command and the \a doc.
356 \a arg is the argument to the metacommand.
357
358 \a node is guaranteed to be non-null.
359 */
360void CppCodeParser::processMetaCommand(const Doc &doc, const QString &command,
361 const ArgPair &argPair, Node *node)
362{
363 QDocDatabase* database = QDocDatabase::qdocDB();
364
365 QString arg = argPair.first;
366 if (command == COMMAND_INHEADERFILE) {
367 // TODO: [incorrect-constructs][header-arg]
368 // The emptiness check for arg is required as,
369 // currently, DocParser fancies passing (without any warning)
370 // incorrect constructs doen the chain, such as an
371 // "\inheaderfile" command with no argument.
372 //
373 // As it is the case here, we require further sanity checks to
374 // preserve some of the semantic for the later phases.
375 // This generally has a ripple effect on the whole codebase,
376 // making it more complex and increasesing the surface of bugs.
377 //
378 // The following emptiness check should be removed as soon as
379 // DocParser is enhanced with correct semantics.
380 if (node->isAggregate() && !arg.isEmpty())
381 static_cast<Aggregate *>(node)->setIncludeFile(arg);
382 else
383 doc.location().warning(QStringLiteral("Ignored '\\%1'").arg(COMMAND_INHEADERFILE));
384 } else if (command == COMMAND_OVERLOAD) {
385 /*
386 Note that this might set the overload flag of the
387 primary function. This is ok because the overload
388 flags and overload numbers will be resolved later
389 in Aggregate::normalizeOverloads().
390 */
391 if (node->isFunction())
392 static_cast<FunctionNode *>(node)->setOverloadFlag();
393 else if (node->isSharedCommentNode())
394 static_cast<SharedCommentNode *>(node)->setOverloadFlags();
395 else
396 doc.location().warning(QStringLiteral("Ignored '\\%1'").arg(COMMAND_OVERLOAD));
397 } else if (command == COMMAND_REIMP) {
398 if (node->parent() && !node->parent()->isInternal()) {
399 if (node->isFunction()) {
400 auto *fn = static_cast<FunctionNode *>(node);
401 // The clang visitor class will have set the
402 // qualified name of the overridden function.
403 // If the name of the overridden function isn't
404 // set, issue a warning.
405 if (fn->overridesThis().isEmpty() && CodeParser::isWorthWarningAbout(doc)) {
406 doc.location().warning(
407 QStringLiteral("Cannot find base function for '\\%1' in %2()")
408 .arg(COMMAND_REIMP, args: node->name()),
409 QStringLiteral("The function either doesn't exist in any "
410 "base class with the same signature or it "
411 "exists but isn't virtual."));
412 }
413 fn->setReimpFlag();
414 } else {
415 doc.location().warning(
416 QStringLiteral("Ignored '\\%1' in %2").arg(COMMAND_REIMP, args: node->name()));
417 }
418 }
419 } else if (command == COMMAND_RELATES) {
420 QStringList path = arg.split(sep: "::");
421 Aggregate *aggregate = database->findRelatesNode(path);
422 if (aggregate == nullptr)
423 aggregate = new ProxyNode(node->root(), arg);
424
425 if (node->parent() == aggregate) { // node is already a child of aggregate
426 doc.location().warning(QStringLiteral("Invalid '\\%1' (already a member of '%2')")
427 .arg(COMMAND_RELATES, args&: arg));
428 } else {
429 if (node->isAggregate()) {
430 doc.location().warning(QStringLiteral("Invalid '\\%1' not allowed in '\\%2'")
431 .arg(COMMAND_RELATES, args: node->nodeTypeString()));
432 } else if (!node->isRelatedNonmember() &&
433 !node->parent()->isNamespace() && !node->parent()->isHeader()) {
434 if (!doc.isInternal()) {
435 doc.location().warning(QStringLiteral("Invalid '\\%1' ('%2' must be global)")
436 .arg(COMMAND_RELATES, args: node->name()));
437 }
438 } else if (!node->isRelatedNonmember() && !node->parent()->isHeader()) {
439 aggregate->adoptChild(child: node);
440 node->setRelatedNonmember(true);
441 } else {
442 /*
443 There are multiple \relates commands. This
444 one is not the first, so clone the node as
445 a child of aggregate.
446 */
447 Node *clone = node->clone(aggregate);
448 if (clone == nullptr) {
449 doc.location().warning(
450 QStringLiteral("Invalid '\\%1' (multiple uses not allowed in '%2')")
451 .arg(COMMAND_RELATES, args: node->nodeTypeString()));
452 } else {
453 clone->setRelatedNonmember(true);
454 }
455 }
456 }
457 } else if (command == COMMAND_NEXTPAGE) {
458 CodeParser::setLink(node, linkType: Node::NextLink, arg);
459 } else if (command == COMMAND_PREVIOUSPAGE) {
460 CodeParser::setLink(node, linkType: Node::PreviousLink, arg);
461 } else if (command == COMMAND_STARTPAGE) {
462 CodeParser::setLink(node, linkType: Node::StartLink, arg);
463 } else if (command == COMMAND_QMLINHERITS) {
464 if (node->name() == arg)
465 doc.location().warning(QStringLiteral("%1 tries to inherit itself").arg(a: arg));
466 else if (node->isQmlType()) {
467 auto *qmlType = static_cast<QmlTypeNode *>(node);
468 qmlType->setQmlBaseName(arg);
469 }
470 } else if (command == COMMAND_QMLINSTANTIATES) {
471 if (node->isQmlType()) {
472 ClassNode *classNode = database->findClassNode(path: arg.split(sep: "::"));
473 if (classNode)
474 node->setClassNode(classNode);
475 else if (m_showLinkErrors) {
476 doc.location().warning(
477 QStringLiteral("C++ class %2 not found: \\%1 %2")
478 .arg(args: command, args&: arg));
479 }
480 } else {
481 doc.location().warning(
482 QStringLiteral("\\%1 is only allowed in \\%2")
483 .arg(args: command, COMMAND_QMLTYPE));
484 }
485 } else if (command == COMMAND_DEFAULT) {
486 if (!node->isQmlProperty()) {
487 doc.location().warning(QStringLiteral("Ignored '\\%1', applies only to '\\%2'")
488 .arg(args: command, COMMAND_QMLPROPERTY));
489 } else if (arg.isEmpty()) {
490 doc.location().warning(QStringLiteral("Expected an argument for '\\%1' (maybe you meant '\\%2'?)")
491 .arg(args: command, COMMAND_QMLDEFAULT));
492 } else {
493 static_cast<QmlPropertyNode *>(node)->setDefaultValue(arg);
494 }
495 } else if (command == COMMAND_QMLDEFAULT) {
496 node->markDefault();
497 } else if (command == COMMAND_QMLREADONLY) {
498 node->markReadOnly(true);
499 } else if (command == COMMAND_QMLREQUIRED) {
500 if (!node->isQmlProperty())
501 doc.location().warning(QStringLiteral("Ignored '\\%1'").arg(COMMAND_QMLREQUIRED));
502 else
503 static_cast<QmlPropertyNode *>(node)->setRequired();
504 } else if ((command == COMMAND_QMLABSTRACT) || (command == COMMAND_ABSTRACT)) {
505 if (node->isQmlType())
506 node->setAbstract(true);
507 } else if (command == COMMAND_DEPRECATED) {
508 node->setStatus(Node::Deprecated);
509 if (!argPair.second.isEmpty())
510 node->setDeprecatedSince(argPair.second);
511 } else if (command == COMMAND_INGROUP || command == COMMAND_INPUBLICGROUP) {
512 // Note: \ingroup and \inpublicgroup are the same (and now recognized as such).
513 database->addToGroup(name: arg, node);
514 } else if (command == COMMAND_INMODULE) {
515 database->addToModule(name: arg, node);
516 } else if (command == COMMAND_INQMLMODULE) {
517 database->addToQmlModule(name: arg, node);
518 } else if (command == COMMAND_OBSOLETE) {
519 node->setStatus(Node::Deprecated);
520 } else if (command == COMMAND_NONREENTRANT) {
521 node->setThreadSafeness(Node::NonReentrant);
522 } else if (command == COMMAND_PRELIMINARY) {
523 // \internal wins.
524 if (!node->isInternal())
525 node->setStatus(Node::Preliminary);
526 } else if (command == COMMAND_INTERNAL) {
527 if (!Config::instance().showInternal())
528 node->markInternal();
529 } else if (command == COMMAND_REENTRANT) {
530 node->setThreadSafeness(Node::Reentrant);
531 } else if (command == COMMAND_SINCE) {
532 node->setSince(arg);
533 } else if (command == COMMAND_WRAPPER) {
534 node->setWrapper();
535 } else if (command == COMMAND_THREADSAFE) {
536 node->setThreadSafeness(Node::ThreadSafe);
537 } else if (command == COMMAND_TITLE) {
538 if (!node->setTitle(arg))
539 doc.location().warning(QStringLiteral("Ignored '\\%1'").arg(COMMAND_TITLE));
540 else if (node->isExample())
541 database->addExampleNode(n: static_cast<ExampleNode *>(node));
542 } else if (command == COMMAND_SUBTITLE) {
543 if (!node->setSubtitle(arg))
544 doc.location().warning(QStringLiteral("Ignored '\\%1'").arg(COMMAND_SUBTITLE));
545 } else if (command == COMMAND_QTVARIABLE) {
546 node->setQtVariable(arg);
547 if (!node->isModule() && !node->isQmlModule())
548 doc.location().warning(
549 QStringLiteral(
550 "Command '\\%1' is only meaningful in '\\module' and '\\qmlmodule'.")
551 .arg(COMMAND_QTVARIABLE));
552 } else if (command == COMMAND_QTCMAKEPACKAGE) {
553 node->setQtCMakeComponent(arg);
554 if (!node->isModule())
555 doc.location().warning(
556 QStringLiteral("Command '\\%1' is only meaningful in '\\module'.")
557 .arg(COMMAND_QTCMAKEPACKAGE));
558 } else if (command == COMMAND_MODULESTATE ) {
559 if (!node->isModule() && !node->isQmlModule()) {
560 doc.location().warning(
561 QStringLiteral(
562 "Command '\\%1' is only meaningful in '\\module' and '\\qmlmodule'.")
563 .arg(COMMAND_MODULESTATE));
564 } else {
565 static_cast<CollectionNode*>(node)->setState(arg);
566 }
567 } else if (command == COMMAND_NOAUTOLIST) {
568 if (!node->isCollectionNode() && !node->isExample()) {
569 doc.location().warning(
570 QStringLiteral(
571 "Command '\\%1' is only meaningful in '\\module', '\\qmlmodule', `\\group` and `\\example`.")
572 .arg(COMMAND_NOAUTOLIST));
573 } else {
574 static_cast<PageNode*>(node)->setNoAutoList(true);
575 }
576 } else if (command == COMMAND_ATTRIBUTION) {
577 // TODO: This condition is not currently exact enough, as it
578 // will allow any non-aggregate `PageNode` to use the command,
579 // For example, an `ExampleNode`.
580 //
581 // The command is intended only for internal usage by
582 // "qattributionscanner" and should only work on `PageNode`s
583 // that are generated from a "\page" command.
584 //
585 // It is already possible to provide a more restricted check,
586 // albeit in a somewhat dirty way. It is not expected that
587 // this warning will have any particular use.
588 // If it so happens that a case where the too-broad scope of
589 // the warning is a problem or hides a bug, modify the
590 // condition to be restrictive enough.
591 // Otherwise, wait until a more torough look at QDoc's
592 // internal representations an way to enable "Attribution
593 // Pages" is performed before looking at the issue again.
594 if (!node->isTextPageNode()) {
595 doc.location().warning(message: u"Command '\\%1' is only meaningful in '\\%2'"_s.arg(COMMAND_ATTRIBUTION, COMMAND_PAGE));
596 } else { static_cast<PageNode*>(node)->markAttribution(); }
597 }
598}
599
600/*!
601 The topic command has been processed, and now \a doc and
602 \a node are passed to this function to get the metacommands
603 from \a doc and process them one at a time. \a node is the
604 node where \a doc resides.
605 */
606void CppCodeParser::processMetaCommands(const Doc &doc, Node *node)
607{
608 const QStringList metaCommandsUsed = doc.metaCommandsUsed().values();
609 for (const auto &command : metaCommandsUsed) {
610 const ArgList args = doc.metaCommandArgs(metaCommand: command);
611 for (const auto &arg : args)
612 processMetaCommand(doc, command, argPair: arg, node);
613 }
614}
615
616/*!
617 Parse QML signal/method topic commands.
618 */
619FunctionNode *CppCodeParser::parseOtherFuncArg(const QString &topic, const Location &location,
620 const QString &funcArg)
621{
622 QString funcName;
623 QString returnType;
624
625 qsizetype leftParen = funcArg.indexOf(c: QChar('('));
626 if (leftParen > 0)
627 funcName = funcArg.left(n: leftParen);
628 else
629 funcName = funcArg;
630 qsizetype firstBlank = funcName.indexOf(c: QChar(' '));
631 if (firstBlank > 0) {
632 returnType = funcName.left(n: firstBlank);
633 funcName = funcName.right(n: funcName.size() - firstBlank - 1);
634 }
635
636 QStringList colonSplit(funcName.split(sep: "::"));
637 if (colonSplit.size() < 2) {
638 QString msg = "Unrecognizable QML module/component qualifier for " + funcArg;
639 location.warning(message: msg.toLatin1().data());
640 return nullptr;
641 }
642 QString moduleName;
643 QString elementName;
644 if (colonSplit.size() > 2) {
645 moduleName = colonSplit[0];
646 elementName = colonSplit[1];
647 } else {
648 elementName = colonSplit[0];
649 }
650 funcName = colonSplit.last();
651
652 QDocDatabase* database = QDocDatabase::qdocDB();
653
654 Aggregate *aggregate = database->findQmlType(qmid: moduleName, name: elementName);
655 if (aggregate == nullptr)
656 return nullptr;
657
658 QString params;
659 QStringList leftParenSplit = funcArg.split(sep: '(');
660 if (leftParenSplit.size() > 1) {
661 QStringList rightParenSplit = leftParenSplit[1].split(sep: ')');
662 if (!rightParenSplit.empty())
663 params = rightParenSplit[0];
664 }
665
666 FunctionNode::Metaness metaness = FunctionNode::getMetanessFromTopic(topic);
667 bool attached = topic.contains(s: QLatin1String("attached"));
668 auto *fn = new FunctionNode(metaness, aggregate, funcName, attached);
669 fn->setAccess(Access::Public);
670 fn->setLocation(location);
671 fn->setReturnType(returnType);
672 fn->setParameters(params);
673 return fn;
674}
675
676/*!
677 Parse the macro arguments in \a macroArg ad hoc, without using
678 any actual parser. If successful, return a pointer to the new
679 FunctionNode for the macro. Otherwise return null. \a location
680 is used for reporting errors.
681 */
682FunctionNode *CppCodeParser::parseMacroArg(const Location &location, const QString &macroArg)
683{
684 QDocDatabase* database = QDocDatabase::qdocDB();
685
686 QStringList leftParenSplit = macroArg.split(sep: '(');
687 if (leftParenSplit.isEmpty())
688 return nullptr;
689 QString macroName;
690 FunctionNode *oldMacroNode = nullptr;
691 QStringList blankSplit = leftParenSplit[0].split(sep: ' ');
692 if (!blankSplit.empty()) {
693 macroName = blankSplit.last();
694 oldMacroNode = database->findMacroNode(t: macroName);
695 }
696 QString returnType;
697 if (blankSplit.size() > 1) {
698 blankSplit.removeLast();
699 returnType = blankSplit.join(sep: ' ');
700 }
701 QString params;
702 if (leftParenSplit.size() > 1) {
703 const QString &afterParen = leftParenSplit.at(i: 1);
704 qsizetype rightParen = afterParen.indexOf(c: ')');
705 if (rightParen >= 0)
706 params = afterParen.left(n: rightParen);
707 }
708 int i = 0;
709 while (i < macroName.size() && !macroName.at(i).isLetter())
710 i++;
711 if (i > 0) {
712 returnType += QChar(' ') + macroName.left(n: i);
713 macroName = macroName.mid(position: i);
714 }
715 FunctionNode::Metaness metaness = FunctionNode::MacroWithParams;
716 if (params.isEmpty())
717 metaness = FunctionNode::MacroWithoutParams;
718 auto *macro = new FunctionNode(metaness, database->primaryTreeRoot(), macroName);
719 macro->setAccess(Access::Public);
720 macro->setLocation(location);
721 macro->setReturnType(returnType);
722 macro->setParameters(params);
723 if (macro->compare(node: oldMacroNode)) {
724 location.warning(QStringLiteral("\\macro %1 documented more than once")
725 .arg(a: macroArg), QStringLiteral("also seen here: %1")
726 .arg(a: oldMacroNode->doc().location().toString()));
727 }
728 return macro;
729}
730
731void CppCodeParser::setExampleFileLists(ExampleNode *en)
732{
733 Config &config = Config::instance();
734 QString fullPath = config.getExampleProjectFile(examplePath: en->name());
735 if (fullPath.isEmpty()) {
736 QString details = QLatin1String("Example directories: ")
737 + config.getCanonicalPathList(CONFIG_EXAMPLEDIRS).join(sep: QLatin1Char(' '));
738 en->location().warning(
739 QStringLiteral("Cannot find project file for example '%1'").arg(a: en->name()),
740 details);
741 return;
742 }
743
744 QDir exampleDir(QFileInfo(fullPath).dir());
745
746 QStringList exampleFiles = Config::getFilesHere(dir: exampleDir.path(), nameFilter: m_exampleNameFilter,
747 location: Location(), excludedDirs: m_excludeDirs, excludedFiles: m_excludeFiles);
748 // Search for all image files under the example project, excluding doc/images directory.
749 QSet<QString> excludeDocDirs(m_excludeDirs);
750 excludeDocDirs.insert(value: exampleDir.path() + QLatin1String("/doc/images"));
751 QStringList imageFiles = Config::getFilesHere(dir: exampleDir.path(), nameFilter: m_exampleImageFilter,
752 location: Location(), excludedDirs: excludeDocDirs, excludedFiles: m_excludeFiles);
753 if (!exampleFiles.isEmpty()) {
754 // move main.cpp to the end, if it exists
755 QString mainCpp;
756
757 const auto isGeneratedOrMainCpp = [&mainCpp](const QString &fileName) {
758 if (fileName.endsWith(s: "/main.cpp")) {
759 if (mainCpp.isEmpty())
760 mainCpp = fileName;
761 return true;
762 }
763 return fileName.contains(s: "/qrc_") || fileName.contains(s: "/moc_")
764 || fileName.contains(s: "/ui_");
765 };
766
767 exampleFiles.erase(
768 abegin: std::remove_if(first: exampleFiles.begin(), last: exampleFiles.end(), pred: isGeneratedOrMainCpp),
769 aend: exampleFiles.end());
770
771 if (!mainCpp.isEmpty())
772 exampleFiles.append(t: mainCpp);
773
774 // Add any resource and project files
775 exampleFiles += Config::getFilesHere(dir: exampleDir.path(),
776 nameFilter: QLatin1String("*.qrc *.pro *.qmlproject *.pyproject CMakeLists.txt qmldir"),
777 location: Location(), excludedDirs: m_excludeDirs, excludedFiles: m_excludeFiles);
778 }
779
780 const qsizetype pathLen = exampleDir.path().size() - en->name().size();
781 for (auto &file : exampleFiles)
782 file = file.mid(position: pathLen);
783 for (auto &file : imageFiles)
784 file = file.mid(position: pathLen);
785
786 en->setFiles(files: exampleFiles, projectFile: fullPath.mid(position: pathLen));
787 en->setImages(imageFiles);
788}
789
790/*!
791 returns true if \a t is \e {qmlsignal}, \e {qmlmethod},
792 \e {qmlattachedsignal}, or \e {qmlattachedmethod}.
793 */
794bool CppCodeParser::isQMLMethodTopic(const QString &t)
795{
796 return (t == COMMAND_QMLSIGNAL || t == COMMAND_QMLMETHOD || t == COMMAND_QMLATTACHEDSIGNAL
797 || t == COMMAND_QMLATTACHEDMETHOD);
798}
799
800/*!
801 Returns true if \a t is \e {qmlproperty}, \e {qmlpropertygroup},
802 or \e {qmlattachedproperty}.
803 */
804bool CppCodeParser::isQMLPropertyTopic(const QString &t)
805{
806 return (t == COMMAND_QMLPROPERTY || t == COMMAND_QMLATTACHEDPROPERTY);
807}
808
809void CppCodeParser::processTopicArgs(const Doc &doc, const QString &topic, NodeList &nodes,
810 DocList &docs)
811{
812
813 QDocDatabase* database = QDocDatabase::qdocDB();
814
815 if (isQMLPropertyTopic(t: topic)) {
816 processQmlProperties(doc, nodes, docs);
817 } else {
818 ArgList args = doc.metaCommandArgs(metaCommand: topic);
819 Node *node = nullptr;
820 if (args.size() == 1) {
821 if (topic == COMMAND_FN) {
822 if (Config::instance().showInternal() || !doc.isInternal())
823 node = CodeParser::parserForLanguage(language: "Clang")->parseFnArg(doc.location(), args[0].first, args[0].second);
824 } else if (topic == COMMAND_MACRO) {
825 node = parseMacroArg(location: doc.location(), macroArg: args[0].first);
826 } else if (isQMLMethodTopic(t: topic)) {
827 node = parseOtherFuncArg(topic, location: doc.location(), funcArg: args[0].first);
828 } else if (topic == COMMAND_DONTDOCUMENT) {
829 database->primaryTree()->addToDontDocumentMap(arg&: args[0].first);
830 } else {
831 node = processTopicCommand(doc, command: topic, arg: args[0]);
832 }
833 if (node != nullptr) {
834 nodes.append(t: node);
835 docs.append(t: doc);
836 }
837 } else if (args.size() > 1) {
838 QList<SharedCommentNode *> sharedCommentNodes;
839 for (const auto &arg : std::as_const(t&: args)) {
840 node = nullptr;
841 if (topic == COMMAND_FN) {
842 if (Config::instance().showInternal() || !doc.isInternal())
843 node = CodeParser::parserForLanguage(language: "Clang")->parseFnArg(doc.location(), arg.first, arg.second);
844 } else if (topic == COMMAND_MACRO) {
845 node = parseMacroArg(location: doc.location(), macroArg: arg.first);
846 } else if (isQMLMethodTopic(t: topic)) {
847 node = parseOtherFuncArg(topic, location: doc.location(), funcArg: arg.first);
848 } else {
849 node = processTopicCommand(doc, command: topic, arg);
850 }
851 if (node != nullptr) {
852 bool found = false;
853 for (SharedCommentNode *scn : sharedCommentNodes) {
854 if (scn->parent() == node->parent()) {
855 scn->append(node);
856 found = true;
857 break;
858 }
859 }
860 if (!found) {
861 auto *scn = new SharedCommentNode(node);
862 sharedCommentNodes.append(t: scn);
863 nodes.append(t: scn);
864 docs.append(t: doc);
865 }
866 processMetaCommands(doc, node);
867 }
868 }
869 for (auto *scn : sharedCommentNodes)
870 scn->sort();
871 }
872 }
873}
874
875/*!
876 For each node that is part of C++ API and produces a documentation
877 page, this function ensures that the node belongs to a module.
878 */
879static void checkModuleInclusion(Node *n)
880{
881 if (n->physicalModuleName().isEmpty()) {
882 if (n->isInAPI() && !n->name().isEmpty()) {
883 switch (n->nodeType()) {
884 case Node::Class:
885 case Node::Struct:
886 case Node::Union:
887 case Node::Namespace:
888 case Node::HeaderFile:
889 break;
890 default:
891 return;
892 }
893 n->setPhysicalModuleName(Generator::defaultModuleName());
894 QDocDatabase::qdocDB()->addToModule(name: Generator::defaultModuleName(), node: n);
895 n->doc().location().warning(
896 QStringLiteral("Documentation for %1 '%2' has no \\inmodule command; "
897 "using project name by default: %3")
898 .arg(args: Node::nodeTypeString(t: n->nodeType()), args: n->name(),
899 args: n->physicalModuleName()));
900 }
901 }
902}
903
904void CppCodeParser::processMetaCommands(NodeList &nodes, DocList &docs)
905{
906 QList<Doc>::Iterator d = docs.begin();
907 for (const auto &node : nodes) {
908 if (node != nullptr) {
909 processMetaCommands(doc: *d, node);
910 node->setDoc(doc: *d);
911 checkModuleInclusion(n: node);
912 if (node->isAggregate()) {
913 auto *aggregate = static_cast<Aggregate *>(node);
914
915 if (!aggregate->includeFile()) {
916 Aggregate *parent = aggregate;
917 while (parent->physicalModuleName().isEmpty() && (parent->parent() != nullptr))
918 parent = parent->parent();
919
920 if (parent == aggregate)
921 // TODO: Understand if the name can be empty.
922 // In theory it should not be possible as
923 // there would be no aggregate to refer to
924 // such that this code is never reached.
925 //
926 // If the name can be empty, this would
927 // endanger users of the include file down the
928 // line, forcing them to ensure that, further
929 // to there being an actual include file, that
930 // include file is not an empty string, such
931 // that we would require a different way to
932 // generate the include file here.
933 aggregate->setIncludeFile(aggregate->name());
934 else if (aggregate->includeFile())
935 aggregate->setIncludeFile(*parent->includeFile());
936 }
937 }
938 }
939 ++d;
940 }
941}
942
943/*!
944 * \internal
945 * \brief Checks if there are too many topic commands in \a doc.
946 *
947 * This method compares the commands used in \a doc with the set of topic
948 * commands. If zero or one topic command is found, or if all found topic
949 * commands are {\\qml*}-commands, the method returns \c false.
950 *
951 * If more than one topic command is found, QDoc issues a warning and the list
952 * of topic commands used in \a doc, and the method returns \c true.
953 */
954bool CppCodeParser::hasTooManyTopics(const Doc &doc) const
955{
956 const QSet<QString> topicCommandsUsed = CppCodeParser::topic_commands & doc.metaCommandsUsed();
957
958 if (topicCommandsUsed.empty() || topicCommandsUsed.size() == 1)
959 return false;
960 if (std::all_of(first: topicCommandsUsed.cbegin(), last: topicCommandsUsed.cend(),
961 pred: [](const auto &cmd) { return cmd.startsWith(QLatin1String("qml")); }))
962 return false;
963
964 const QStringList commands = topicCommandsUsed.values();
965 const QString topicCommands{ std::accumulate(
966 first: commands.cbegin(), last: commands.cend(), init: QString{},
967 binary_op: [index = qsizetype{ 0 }, numberOfCommands = commands.size()](
968 const QString &accumulator, const QString &topic) mutable -> QString {
969 return accumulator + QLatin1String("\\") + topic
970 + Utilities::separator(wordPosition: index++, numberOfWords: numberOfCommands);
971 }) };
972
973 doc.location().warning(
974 QStringLiteral("Multiple topic commands found in comment: %1").arg(a: topicCommands));
975 return true;
976}
977
978QT_END_NAMESPACE
979

source code of qttools/src/qdoc/qdoc/cppcodeparser.cpp