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

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