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 | |
27 | using namespace Qt::Literals::StringLiterals; |
28 | |
29 | QT_BEGIN_NAMESPACE |
30 | |
31 | /* qmake ignore Q_OBJECT */ |
32 | |
33 | QSet<QString> CppCodeParser::m_excludeDirs; |
34 | QSet<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 | */ |
40 | static 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 | |
48 | typedef bool (Node::*NodeTypeTestFunc)() const; |
49 | static 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 | |
57 | CppCodeParser::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 | */ |
90 | CppCodeParser::~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 | */ |
99 | Node *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 | */ |
239 | bool 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 | */ |
269 | void 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 | */ |
360 | void 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 | */ |
606 | void 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 | */ |
619 | FunctionNode *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 | */ |
682 | FunctionNode *CppCodeParser::parseMacroArg(const Location &location, const QString ¯oArg) |
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 | |
731 | void 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 | */ |
794 | bool 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 | */ |
804 | bool CppCodeParser::isQMLPropertyTopic(const QString &t) |
805 | { |
806 | return (t == COMMAND_QMLPROPERTY || t == COMMAND_QMLATTACHEDPROPERTY); |
807 | } |
808 | |
809 | void 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 *> ; |
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 | */ |
879 | static 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 | |
904 | void 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 | */ |
954 | bool 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 | |
978 | QT_END_NAMESPACE |
979 | |