| 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 | |
| 31 | using namespace Qt::Literals::StringLiterals; |
| 32 | |
| 33 | QT_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 | */ |
| 39 | static 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 | |
| 47 | typedef bool (Node::*NodeTypeTestFunc)() const; |
| 48 | static 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 | |
| 56 | CppCodeParser::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 | */ |
| 84 | Node *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 | */ |
| 210 | QmlTypeNode *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 | |
| 226 | std::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 | */ |
| 314 | void 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 | */ |
| 571 | void 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 | */ |
| 594 | void 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 | */ |
| 634 | EnumNode *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 | */ |
| 663 | FunctionNode *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 | */ |
| 722 | FunctionNode *CppCodeParser::parseMacroArg(const Location &location, const QString ¯oArg) |
| 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 | |
| 773 | void 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 | */ |
| 838 | bool 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 | */ |
| 848 | bool CppCodeParser::isQMLPropertyTopic(const QString &t) |
| 849 | { |
| 850 | return (t == COMMAND_QMLPROPERTY || t == COMMAND_QMLATTACHEDPROPERTY); |
| 851 | } |
| 852 | |
| 853 | std::pair<std::vector<TiedDocumentation>, std::vector<FnMatchError>> |
| 854 | CppCodeParser::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 *> ; |
| 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 | */ |
| 941 | static 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 | |
| 966 | void 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 | |
| 1001 | void 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 | |
| 1035 | QT_END_NAMESPACE |
| 1036 | |