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 "generator.h"
5
6#include "access.h"
7#include "aggregate.h"
8#include "classnode.h"
9#include "codemarker.h"
10#include "collectionnode.h"
11#include "config.h"
12#include "doc.h"
13#include "editdistance.h"
14#include "enumnode.h"
15#include "examplenode.h"
16#include "functionnode.h"
17#include "node.h"
18#include "openedlist.h"
19#include "propertynode.h"
20#include "qdocdatabase.h"
21#include "qmltypenode.h"
22#include "quoter.h"
23#include "sharedcommentnode.h"
24#include "tokenizer.h"
25#include "typedefnode.h"
26#include "utilities.h"
27
28#include <QtCore/qdebug.h>
29#include <QtCore/qdir.h>
30#include <QtCore/qregularexpression.h>
31
32#ifndef QT_BOOTSTRAPPED
33# include "QtCore/qurl.h"
34#endif
35
36#include <string>
37
38using namespace std::literals::string_literals;
39
40QT_BEGIN_NAMESPACE
41
42using namespace Qt::StringLiterals;
43
44Generator *Generator::s_currentGenerator;
45QMap<QString, QMap<QString, QString>> Generator::s_fmtLeftMaps;
46QMap<QString, QMap<QString, QString>> Generator::s_fmtRightMaps;
47QList<Generator *> Generator::s_generators;
48QString Generator::s_outDir;
49QString Generator::s_outSubdir;
50QStringList Generator::s_outFileNames;
51QSet<QString> Generator::s_outputFormats;
52QHash<QString, QString> Generator::s_outputPrefixes;
53QHash<QString, QString> Generator::s_outputSuffixes;
54QString Generator::s_project;
55bool Generator::s_noLinkErrors = false;
56bool Generator::s_autolinkErrors = false;
57bool Generator::s_redirectDocumentationToDevNull = false;
58bool Generator::s_useOutputSubdirs = true;
59QmlTypeNode *Generator::s_qmlTypeContext = nullptr;
60
61static QRegularExpression tag("</?@[^>]*>");
62static QLatin1String amp("&amp;");
63static QLatin1String gt("&gt;");
64static QLatin1String lt("&lt;");
65static QLatin1String quot("&quot;");
66
67/*!
68 Constructs the generator base class. Prepends the newly
69 constructed generator to the list of output generators.
70 Sets a pointer to the QDoc database singleton, which is
71 available to the generator subclasses.
72 */
73Generator::Generator(FileResolver& file_resolver)
74 : file_resolver{file_resolver}
75{
76 m_qdb = QDocDatabase::qdocDB();
77 s_generators.prepend(t: this);
78}
79
80/*!
81 Destroys the generator after removing it from the list of
82 output generators.
83 */
84Generator::~Generator()
85{
86 s_generators.removeAll(t: this);
87}
88
89void Generator::appendFullName(Text &text, const Node *apparentNode, const Node *relative,
90 const Node *actualNode)
91{
92 if (actualNode == nullptr)
93 actualNode = apparentNode;
94 text << Atom(Atom::LinkNode, CodeMarker::stringForNode(node: actualNode))
95 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
96 << Atom(Atom::String, apparentNode->plainFullName(relative))
97 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
98}
99
100void Generator::appendFullName(Text &text, const Node *apparentNode, const QString &fullName,
101 const Node *actualNode)
102{
103 if (actualNode == nullptr)
104 actualNode = apparentNode;
105 text << Atom(Atom::LinkNode, CodeMarker::stringForNode(node: actualNode))
106 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << Atom(Atom::String, fullName)
107 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
108}
109
110/*!
111 Append the signature for the function named in \a node to
112 \a text, so that is a link to the documentation for that
113 function.
114 */
115void Generator::appendSignature(Text &text, const Node *node)
116{
117 text << Atom(Atom::LinkNode, CodeMarker::stringForNode(node))
118 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
119 << Atom(Atom::String, node->signature(Node::SignaturePlain))
120 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
121}
122
123/*!
124 Generate a bullet list of function signatures. The function
125 nodes are in \a nodes. It uses the \a relative node and the
126 \a marker for the generation.
127 */
128void Generator::signatureList(const NodeList &nodes, const Node *relative, CodeMarker *marker)
129{
130 Text text;
131 int count = 0;
132 text << Atom(Atom::ListLeft, QString("bullet"));
133 for (const auto &node : nodes) {
134 text << Atom(Atom::ListItemNumber, QString::number(++count));
135 text << Atom(Atom::ListItemLeft, QString("bullet"));
136 appendSignature(text, node);
137 text << Atom(Atom::ListItemRight, QString("bullet"));
138 }
139 text << Atom(Atom::ListRight, QString("bullet"));
140 generateText(text, relative, marker);
141}
142
143int Generator::appendSortedNames(Text &text, const ClassNode *cn, const QList<RelatedClass> &rc)
144{
145 QMap<QString, Text> classMap;
146 for (const auto &relatedClass : rc) {
147 ClassNode *rcn = relatedClass.m_node;
148 if (rcn && rcn->isInAPI()) {
149 Text className;
150 appendFullName(text&: className, apparentNode: rcn, relative: cn);
151 classMap[className.toString().toLower()] = className;
152 }
153 }
154
155 int index = 0;
156 const QStringList classNames = classMap.keys();
157 for (const auto &className : classNames) {
158 text << classMap[className];
159 text << Utilities::comma(wordPosition: index++, numberOfWords: classNames.size());
160 }
161 return index;
162}
163
164int Generator::appendSortedQmlNames(Text &text, const Node *base, const NodeList &subs)
165{
166 QMap<QString, Text> classMap;
167
168 for (const auto sub : subs) {
169 Text text;
170 if (!base->isQtQuickNode() || !sub->isQtQuickNode()
171 || (base->logicalModuleName() == sub->logicalModuleName())) {
172 appendFullName(text, apparentNode: sub, relative: base);
173 classMap[text.toString().toLower()] = text;
174 }
175 }
176
177 int index = 0;
178 const QStringList names = classMap.keys();
179 for (const auto &name : names) {
180 text << classMap[name];
181 text << Utilities::comma(wordPosition: index++, numberOfWords: names.size());
182 }
183 return index;
184}
185
186/*!
187 Creates the file named \a fileName in the output directory
188 and returns a QFile pointing to this file. In particular,
189 this method deals with errors when opening the file:
190 the returned QFile is always valid and can be written to.
191
192 \sa beginSubPage()
193 */
194QFile *Generator::openSubPageFile(const Node *node, const QString &fileName)
195{
196 QString path = outputDir() + QLatin1Char('/');
197 if (Generator::useOutputSubdirs() && !node->outputSubdirectory().isEmpty()
198 && !outputDir().endsWith(s: node->outputSubdirectory())) {
199 path += node->outputSubdirectory() + QLatin1Char('/');
200 }
201 path += fileName;
202
203 auto outPath = s_redirectDocumentationToDevNull ? QStringLiteral("/dev/null") : path;
204 auto outFile = new QFile(outPath);
205
206 if (!s_redirectDocumentationToDevNull && outFile->exists())
207 qCDebug(lcQdoc) << "Output file already exists; overwriting" << qPrintable(outFile->fileName());
208
209 if (!outFile->open(flags: QFile::WriteOnly | QFile::Text)) {
210 node->location().fatal(
211 QStringLiteral("Cannot open output file '%1'").arg(a: outFile->fileName()));
212 }
213
214 qCDebug(lcQdoc, "Writing: %s", qPrintable(path));
215 s_outFileNames << fileName;
216 return outFile;
217}
218
219/*!
220 Creates the file named \a fileName in the output directory.
221 Attaches a QTextStream to the created file, which is written
222 to all over the place using out().
223 */
224void Generator::beginSubPage(const Node *node, const QString &fileName)
225{
226 QFile *outFile = openSubPageFile(node, fileName);
227 auto *out = new QTextStream(outFile);
228 outStreamStack.push(t: out);
229}
230
231/*!
232 Flush the text stream associated with the subpage, and
233 then pop it off the text stream stack and delete it.
234 This terminates output of the subpage.
235 */
236void Generator::endSubPage()
237{
238 outStreamStack.top()->flush();
239 delete outStreamStack.top()->device();
240 delete outStreamStack.pop();
241}
242
243QString Generator::fileBase(const Node *node) const
244{
245 if (!node->isPageNode() && !node->isCollectionNode())
246 node = node->parent();
247
248 if (node->hasFileNameBase())
249 return node->fileNameBase();
250
251 QString base;
252 if (node->isCollectionNode()) {
253 base = node->name() + outputSuffix(node);
254 if (base.endsWith(s: ".html"))
255 base.truncate(pos: base.size() - 5);
256
257 if (node->isQmlModule())
258 base.append(s: "-qmlmodule");
259 else if (node->isModule())
260 base.append(s: "-module");
261 // Why not add "-group" for group pages?
262 } else if (node->isTextPageNode()) {
263 base = node->name();
264 if (base.endsWith(s: ".html"))
265 base.truncate(pos: base.size() - 5);
266
267 if (node->isExample()) {
268 base.prepend(s: s_project.toLower() + QLatin1Char('-'));
269 base.append(s: QLatin1String("-example"));
270 }
271 } else if (node->isQmlType()) {
272 base = node->name();
273 /*
274 To avoid file name conflicts in the html directory,
275 we prepend a prefix (by default, "qml-") and an optional suffix
276 to the file name. The suffix, if one exists, is appended to the
277 module name.
278
279 For historical reasons, skip the module name qualifier for QML value types
280 in order to avoid excess redirects in the online docs. TODO: re-assess
281 */
282 if (!node->logicalModuleName().isEmpty() && !node->isQmlBasicType()
283 && (!node->logicalModule()->isInternal() || m_showInternal))
284 base.prepend(s: node->logicalModuleName() + outputSuffix(node) + QLatin1Char('-'));
285
286 base.prepend(s: outputPrefix(node));
287 } else if (node->isProxyNode()) {
288 base = node->name();
289 base.append(s: "-proxy");
290 } else {
291 const Node *p = node;
292 forever {
293 const Node *pp = p->parent();
294 base.prepend(s: p->name());
295 if (pp == nullptr || pp->name().isEmpty() || pp->isTextPageNode())
296 break;
297 base.prepend(c: QLatin1Char('-'));
298 p = pp;
299 }
300 if (node->isNamespace() && !node->name().isEmpty()) {
301 const auto *ns = static_cast<const NamespaceNode *>(node);
302 if (!ns->isDocumentedHere()) {
303 base.append(s: QLatin1String("-sub-"));
304 base.append(s: ns->tree()->camelCaseModuleName());
305 }
306 }
307 }
308
309 QString canonicalName{ Utilities::asAsciiPrintable(name: base) };
310 Node *n = const_cast<Node *>(node);
311 n->setFileNameBase(canonicalName);
312 return canonicalName;
313}
314
315/*!
316 Constructs an href link from an example file name, which
317 is a \a path to the example file. If \a fileExt is empty
318 (default value), retrieve the file extension from
319 the generator.
320 */
321QString Generator::linkForExampleFile(const QString &path, const QString &fileExt)
322{
323 QString link{path};
324 link.prepend(s: s_project.toLower() + QLatin1Char('-'));
325
326 QString canonicalName{ Utilities::asAsciiPrintable(name: link) };
327 canonicalName.append(c: QLatin1Char('.'));
328 canonicalName.append(s: fileExt.isEmpty() ? fileExtension() : fileExt);
329 return canonicalName;
330}
331
332/*!
333 Helper function to construct a title for a file or image page
334 included in an example.
335*/
336QString Generator::exampleFileTitle(const ExampleNode *relative, const QString &fileName)
337{
338 QString suffix;
339 if (relative->files().contains(str: fileName))
340 suffix = QLatin1String(" Example File");
341 else if (relative->images().contains(str: fileName))
342 suffix = QLatin1String(" Image File");
343 else
344 return suffix;
345
346 return fileName.mid(position: fileName.lastIndexOf(c: QLatin1Char('/')) + 1) + suffix;
347}
348
349/*!
350 If the \a node has a URL, return the URL as the file name.
351 Otherwise, construct the file name from the fileBase() and
352 either the provided \a extension or fileExtension(), and
353 return the constructed name.
354 */
355QString Generator::fileName(const Node *node, const QString &extension) const
356{
357 if (!node->url().isEmpty())
358 return node->url();
359
360 QString name = fileBase(node) + QLatin1Char('.');
361 return name + (extension.isNull() ? fileExtension() : extension);
362}
363
364/*!
365 Clean the given \a ref to be used as an HTML anchor or an \c xml:id.
366 If \a xmlCompliant is set to \c true, a stricter process is used, as XML
367 is more rigorous in what it accepts. Otherwise, if \a xmlCompliant is set to
368 \c false, the basic HTML transformations are applied.
369
370 More specifically, only XML NCNames are allowed
371 (https://www.w3.org/TR/REC-xml-names/#NT-NCName).
372 */
373QString Generator::cleanRef(const QString &ref, bool xmlCompliant)
374{
375 // XML-compliance is ensured in two ways:
376 // - no digit (0-9) at the beginning of an ID (many IDs do not respect this property)
377 // - no colon (:) anywhere in the ID (occurs very rarely)
378
379 QString clean;
380
381 if (ref.isEmpty())
382 return clean;
383
384 clean.reserve(asize: ref.size() + 20);
385 const QChar c = ref[0];
386 const uint u = c.unicode();
387
388 if ((u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z') || (!xmlCompliant && u >= '0' && u <= '9')) {
389 clean += c;
390 } else if (xmlCompliant && u >= '0' && u <= '9') {
391 clean += QLatin1Char('A') + c;
392 } else if (u == '~') {
393 clean += "dtor.";
394 } else if (u == '_') {
395 clean += "underscore.";
396 } else {
397 clean += QLatin1Char('A');
398 }
399
400 for (int i = 1; i < ref.size(); i++) {
401 const QChar c = ref[i];
402 const uint u = c.unicode();
403 if ((u >= 'a' && u <= 'z') || (u >= 'A' && u <= 'Z') || (u >= '0' && u <= '9') || u == '-'
404 || u == '_' || (xmlCompliant && u == ':') || u == '.') {
405 clean += c;
406 } else if (c.isSpace()) {
407 clean += QLatin1Char('-');
408 } else if (u == '!') {
409 clean += "-not";
410 } else if (u == '&') {
411 clean += "-and";
412 } else if (u == '<') {
413 clean += "-lt";
414 } else if (u == '=') {
415 clean += "-eq";
416 } else if (u == '>') {
417 clean += "-gt";
418 } else if (u == '#') {
419 clean += QLatin1Char('#');
420 } else {
421 clean += QLatin1Char('-');
422 clean += QString::number(static_cast<int>(u), base: 16);
423 }
424 }
425 return clean;
426}
427
428QMap<QString, QString> &Generator::formattingLeftMap()
429{
430 return s_fmtLeftMaps[format()];
431}
432
433QMap<QString, QString> &Generator::formattingRightMap()
434{
435 return s_fmtRightMaps[format()];
436}
437
438/*!
439 Returns the full document location.
440 */
441QString Generator::fullDocumentLocation(const Node *node, bool useSubdir)
442{
443 if (node == nullptr)
444 return QString();
445 if (!node->url().isEmpty())
446 return node->url();
447
448 QString parentName;
449 QString anchorRef;
450 QString fdl;
451
452 /*
453 If the useSubdir parameter is set, then the output is
454 being sent to subdirectories of the output directory.
455 Prepend the subdirectory name + '/' to the result.
456 */
457 if (useSubdir) {
458 fdl = node->outputSubdirectory();
459 if (!fdl.isEmpty())
460 fdl.append(c: QLatin1Char('/'));
461 }
462 if (node->isNamespace()) {
463 /*
464 The root namespace has no name - check for this before creating
465 an attribute containing the location of any documentation.
466 */
467 if (!fileBase(node).isEmpty())
468 parentName = fileBase(node) + QLatin1Char('.') + currentGenerator()->fileExtension();
469 else
470 return QString();
471 } else if (node->isQmlType()) {
472 return fileBase(node) + QLatin1Char('.') + currentGenerator()->fileExtension();
473 } else if (node->isTextPageNode() || node->isCollectionNode()) {
474 parentName = fileBase(node) + QLatin1Char('.') + currentGenerator()->fileExtension();
475 } else if (fileBase(node).isEmpty())
476 return QString();
477
478 Node *parentNode = nullptr;
479
480 if ((parentNode = node->parent())) {
481 // use the parent's name unless the parent is the root namespace
482 if (!node->parent()->isNamespace() || !node->parent()->name().isEmpty())
483 parentName = fullDocumentLocation(node: node->parent());
484 }
485
486 switch (node->nodeType()) {
487 case Node::Class:
488 case Node::Struct:
489 case Node::Union:
490 case Node::Namespace:
491 case Node::Proxy:
492 parentName = fileBase(node) + QLatin1Char('.') + currentGenerator()->fileExtension();
493 break;
494 case Node::Function: {
495 const auto *fn = static_cast<const FunctionNode *>(node);
496 switch (fn->metaness()) {
497 case FunctionNode::QmlSignal:
498 anchorRef = QLatin1Char('#') + node->name() + "-signal";
499 break;
500 case FunctionNode::QmlSignalHandler:
501 anchorRef = QLatin1Char('#') + node->name() + "-signal-handler";
502 break;
503 case FunctionNode::QmlMethod:
504 anchorRef = QLatin1Char('#') + node->name() + "-method";
505 break;
506 default:
507 if (fn->isDtor())
508 anchorRef = "#dtor." + fn->name().mid(position: 1);
509 else if (fn->hasOneAssociatedProperty() && fn->doc().isEmpty())
510 return fullDocumentLocation(node: fn->associatedProperties()[0]);
511 else if (fn->overloadNumber() > 0)
512 anchorRef = QLatin1Char('#') + cleanRef(ref: fn->name()) + QLatin1Char('-')
513 + QString::number(fn->overloadNumber());
514 else
515 anchorRef = QLatin1Char('#') + cleanRef(ref: fn->name());
516 break;
517 }
518 break;
519 }
520 /*
521 Use node->name() instead of fileBase(node) as
522 the latter returns the name in lower-case. For
523 HTML anchors, we need to preserve the case.
524 */
525 case Node::Enum:
526 anchorRef = QLatin1Char('#') + node->name() + "-enum";
527 break;
528 case Node::Typedef: {
529 const auto *tdef = static_cast<const TypedefNode *>(node);
530 if (tdef->associatedEnum())
531 return fullDocumentLocation(node: tdef->associatedEnum());
532 } Q_FALLTHROUGH();
533 case Node::TypeAlias:
534 anchorRef = QLatin1Char('#') + node->name() + "-typedef";
535 break;
536 case Node::Property:
537 anchorRef = QLatin1Char('#') + node->name() + "-prop";
538 break;
539 case Node::SharedComment: {
540 if (!node->isPropertyGroup())
541 break;
542 } Q_FALLTHROUGH();
543 case Node::QmlProperty:
544 if (node->isAttached())
545 anchorRef = QLatin1Char('#') + node->name() + "-attached-prop";
546 else
547 anchorRef = QLatin1Char('#') + node->name() + "-prop";
548 break;
549 case Node::Variable:
550 anchorRef = QLatin1Char('#') + node->name() + "-var";
551 break;
552 case Node::QmlType:
553 case Node::Page:
554 case Node::Group:
555 case Node::HeaderFile:
556 case Node::Module:
557 case Node::QmlModule: {
558 parentName = fileBase(node);
559 parentName.replace(before: QLatin1Char('/'), after: QLatin1Char('-'))
560 .replace(before: QLatin1Char('.'), after: QLatin1Char('-'));
561 parentName += QLatin1Char('.') + currentGenerator()->fileExtension();
562 } break;
563 default:
564 break;
565 }
566
567 if (!node->isClassNode() && !node->isNamespace()) {
568 if (node->isDeprecated())
569 parentName.replace(before: QLatin1Char('.') + currentGenerator()->fileExtension(),
570 after: "-obsolete." + currentGenerator()->fileExtension());
571 }
572
573 return fdl + parentName.toLower() + anchorRef;
574}
575
576void Generator::generateAlsoList(const Node *node, CodeMarker *marker)
577{
578 QList<Text> alsoList = node->doc().alsoList();
579 supplementAlsoList(node, alsoList);
580
581 if (!alsoList.isEmpty()) {
582 Text text;
583 text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) << "See also "
584 << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD);
585
586 for (int i = 0; i < alsoList.size(); ++i)
587 text << alsoList.at(i) << Utilities::separator(wordPosition: i, numberOfWords: alsoList.size());
588
589 text << Atom::ParaRight;
590 generateText(text, relative: node, marker);
591 }
592}
593
594const Atom *Generator::generateAtomList(const Atom *atom, const Node *relative, CodeMarker *marker,
595 bool generate, int &numAtoms)
596{
597 while (atom != nullptr) {
598 if (atom->type() == Atom::FormatIf) {
599 int numAtoms0 = numAtoms;
600 bool rightFormat = canHandleFormat(format: atom->string());
601 atom = generateAtomList(atom: atom->next(), relative, marker, generate: generate && rightFormat,
602 numAtoms);
603 if (atom == nullptr)
604 return nullptr;
605
606 if (atom->type() == Atom::FormatElse) {
607 ++numAtoms;
608 atom = generateAtomList(atom: atom->next(), relative, marker, generate: generate && !rightFormat,
609 numAtoms);
610 if (atom == nullptr)
611 return nullptr;
612 }
613
614 if (atom->type() == Atom::FormatEndif) {
615 if (generate && numAtoms0 == numAtoms) {
616 relative->location().warning(QStringLiteral("Output format %1 not handled %2")
617 .arg(args: format(), args: outFileName()));
618 Atom unhandledFormatAtom(Atom::UnhandledFormat, format());
619 generateAtomList(atom: &unhandledFormatAtom, relative, marker, generate, numAtoms);
620 }
621 atom = atom->next();
622 }
623 } else if (atom->type() == Atom::FormatElse || atom->type() == Atom::FormatEndif) {
624 return atom;
625 } else {
626 int n = 1;
627 if (generate) {
628 n += generateAtom(atom, relative, marker);
629 numAtoms += n;
630 }
631 while (n-- > 0)
632 atom = atom->next();
633 }
634 }
635 return nullptr;
636}
637
638/*!
639 Generate the body of the documentation from the qdoc comment
640 found with the entity represented by the \a node.
641 */
642void Generator::generateBody(const Node *node, CodeMarker *marker)
643{
644 const FunctionNode *fn = node->isFunction() ? static_cast<const FunctionNode *>(node) : nullptr;
645 if (!node->hasDoc() && !node->hasSharedDoc()) {
646 /*
647 Test for special function, like a destructor or copy constructor,
648 that has no documentation.
649 */
650 if (fn) {
651 if (fn->isDtor()) {
652 Text text;
653 text << "Destroys the instance of ";
654 text << fn->parent()->name() << ".";
655 if (fn->isVirtual())
656 text << " The destructor is virtual.";
657 out() << "<p>";
658 generateText(text, relative: node, marker);
659 out() << "</p>";
660 } else if (fn->isCtor()) {
661 Text text;
662 text << "Default constructs an instance of ";
663 text << fn->parent()->name() << ".";
664 out() << "<p>";
665 generateText(text, relative: node, marker);
666 out() << "</p>";
667 } else if (fn->isCCtor()) {
668 Text text;
669 text << "Copy constructor.";
670 out() << "<p>";
671 generateText(text, relative: node, marker);
672 out() << "</p>";
673 } else if (fn->isMCtor()) {
674 Text text;
675 text << "Move-copy constructor.";
676 out() << "<p>";
677 generateText(text, relative: node, marker);
678 out() << "</p>";
679 } else if (fn->isCAssign()) {
680 Text text;
681 text << "Copy-assignment operator.";
682 out() << "<p>";
683 generateText(text, relative: node, marker);
684 out() << "</p>";
685 } else if (fn->isMAssign()) {
686 Text text;
687 text << "Move-assignment operator.";
688 out() << "<p>";
689 generateText(text, relative: node, marker);
690 out() << "</p>";
691 } else if (!node->isWrapper() && !node->isMarkedReimp()) {
692 if (!fn->isIgnored()) // undocumented functions added by Q_OBJECT
693 node->location().warning(QStringLiteral("No documentation for '%1'")
694 .arg(a: node->plainSignature()));
695 }
696 } else if (!node->isWrapper() && !node->isMarkedReimp()) {
697 // Don't require documentation of things defined in Q_GADGET
698 if (node->name() != QLatin1String("QtGadgetHelper"))
699 node->location().warning(
700 QStringLiteral("No documentation for '%1'").arg(a: node->plainSignature()));
701 }
702 } else if (!node->isSharingComment()) {
703 // Reimplements clause and type alias info precede body text
704 if (fn && !fn->overridesThis().isEmpty())
705 generateReimplementsClause(fn, marker);
706 else if (node->isProperty()) {
707 if (static_cast<const PropertyNode *>(node)->propertyType() != PropertyNode::PropertyType::StandardProperty)
708 generateAddendum(node, type: BindableProperty, marker);
709 }
710
711 if (!generateText(text: node->doc().body(), relative: node, marker)) {
712 if (node->isMarkedReimp())
713 return;
714 }
715
716 if (fn) {
717 if (fn->isQmlSignal())
718 generateAddendum(node, type: QmlSignalHandler, marker);
719 if (fn->isPrivateSignal())
720 generateAddendum(node, type: PrivateSignal, marker);
721 if (fn->isInvokable())
722 generateAddendum(node, type: Invokable, marker);
723 if (fn->hasAssociatedProperties())
724 generateAddendum(node, type: AssociatedProperties, marker);
725 }
726
727 // Generate warnings
728 if (node->isEnumType()) {
729 const auto *enume = static_cast<const EnumNode *>(node);
730
731 QSet<QString> definedItems;
732 const QList<EnumItem> &items = enume->items();
733 for (const auto &item : items)
734 definedItems.insert(value: item.name());
735
736 const auto &documentedItemList = enume->doc().enumItemNames();
737 QSet<QString> documentedItems(documentedItemList.cbegin(), documentedItemList.cend());
738 const QSet<QString> allItems = definedItems + documentedItems;
739 if (allItems.size() > definedItems.size()
740 || allItems.size() > documentedItems.size()) {
741 for (const auto &it : allItems) {
742 if (!definedItems.contains(value: it)) {
743 QString details;
744 QString best = nearestName(actual: it, candidates: definedItems);
745 if (!best.isEmpty() && !documentedItems.contains(value: best))
746 details = QStringLiteral("Maybe you meant '%1'?").arg(a: best);
747
748 node->doc().location().warning(
749 QStringLiteral("No such enum item '%1' in %2")
750 .arg(args: it, args: node->plainFullName()),
751 details);
752 } else if (!documentedItems.contains(value: it)) {
753 node->doc().location().warning(
754 QStringLiteral("Undocumented enum item '%1' in %2")
755 .arg(args: it, args: node->plainFullName()));
756 }
757 }
758 }
759 } else if (fn) {
760 const QSet<QString> declaredNames = fn->parameters().getNames();
761 const QSet<QString> documentedNames = fn->doc().parameterNames();
762 if (declaredNames != documentedNames) {
763 for (const auto &name : declaredNames) {
764 if (!documentedNames.contains(value: name)) {
765 if (fn->isActive() || fn->isPreliminary()) {
766 if (!fn->isMarkedReimp() && !fn->isOverload()) {
767 fn->doc().location().warning(
768 QStringLiteral("Undocumented parameter '%1' in %2")
769 .arg(args: name, args: node->plainFullName()));
770 }
771 }
772 }
773 }
774 for (const auto &name : documentedNames) {
775 if (!declaredNames.contains(value: name)) {
776 QString best = nearestName(actual: name, candidates: declaredNames);
777 QString details;
778 if (!best.isEmpty())
779 details = QStringLiteral("Maybe you meant '%1'?").arg(a: best);
780 fn->doc().location().warning(QStringLiteral("No such parameter '%1' in %2")
781 .arg(args: name, args: fn->plainFullName()),
782 details);
783 }
784 }
785 }
786 /*
787 This return value check should be implemented
788 for all functions with a return type.
789 mws 13/12/2018
790 */
791 if (!fn->isDeprecated() && fn->returnsBool() && !fn->isMarkedReimp()
792 && !fn->isOverload()) {
793 if (!fn->doc().body().contains(str: "return"))
794 node->doc().location().warning(
795 QStringLiteral("Undocumented return value "
796 "(hint: use 'return' or 'returns' in the text"));
797 }
798 }
799 }
800 generateRequiredLinks(node, marker);
801}
802
803/*!
804 Generates either a link to the project folder for example \a node, or a list
805 of links files/images if 'url.examples config' variable is not defined.
806
807 Does nothing for non-example nodes.
808*/
809void Generator::generateRequiredLinks(const Node *node, CodeMarker *marker)
810{
811 if (!node->isExample())
812 return;
813
814 const auto *en = static_cast<const ExampleNode *>(node);
815 QString exampleUrl{Config::instance().get(CONFIG_URL + Config::dot + CONFIG_EXAMPLES).asString()};
816
817 if (exampleUrl.isEmpty()) {
818 if (!en->noAutoList()) {
819 generateFileList(en, marker, images: false); // files
820 generateFileList(en, marker, images: true); // images
821 }
822 } else {
823 generateLinkToExample(en, marker, exampleUrl);
824 }
825}
826
827/*!
828 Generates an external link to the project folder for example \a node.
829 The path to the example replaces a placeholder '\1' character if
830 one is found in the \a baseUrl string. If no such placeholder is found,
831 the path is appended to \a baseUrl, after a '/' character if \a baseUrl did
832 not already end in one.
833*/
834void Generator::generateLinkToExample(const ExampleNode *en, CodeMarker *marker,
835 const QString &baseUrl)
836{
837 QString exampleUrl(baseUrl);
838 QString link;
839#ifndef QT_BOOTSTRAPPED
840 link = QUrl(exampleUrl).host();
841#endif
842 if (!link.isEmpty())
843 link.prepend(s: " @ ");
844 link.prepend(s: "Example project");
845
846 const QLatin1Char separator('/');
847 const QLatin1Char placeholder('\1');
848 if (!exampleUrl.contains(c: placeholder)) {
849 if (!exampleUrl.endsWith(c: separator))
850 exampleUrl += separator;
851 exampleUrl += placeholder;
852 }
853
854 // Construct a path to the example; <install path>/<example name>
855 QString pathRoot;
856 QStringMultiMap *metaTagMap = en->doc().metaTagMap();
857 if (metaTagMap)
858 pathRoot = metaTagMap->value(key: QLatin1String("installpath"));
859 if (pathRoot.isEmpty())
860 pathRoot = Config::instance().get(CONFIG_EXAMPLESINSTALLPATH).asString();
861 QStringList path = QStringList() << pathRoot << en->name();
862 path.removeAll(t: QString());
863
864 Text text;
865 text << Atom::ParaLeft
866 << Atom(Atom::Link, exampleUrl.replace(c: placeholder, after: path.join(sep: separator)))
867 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << Atom(Atom::String, link)
868 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << Atom::ParaRight;
869
870 generateText(text, relative: nullptr, marker);
871}
872
873void Generator::addImageToCopy(const ExampleNode *en, const ResolvedFile& resolved_file)
874{
875 QDir dirInfo;
876 // TODO: [uncentralized-output-directory-structure]
877 const QString prefix("/images/used-in-examples");
878
879 // TODO: Generators probably should not need to keep track of which files were generated.
880 // Understand if we really need this information and where it should
881 // belong, considering that it should be part of whichever system
882 // would actually store the file itself.
883 s_outFileNames << prefix.mid(position: 1) + "/" + resolved_file.get_query();
884
885
886 // TODO: [uncentralized-output-directory-structure]
887 QString imgOutDir = s_outDir + prefix + "/" + QFileInfo{resolved_file.get_query()}.path();
888 if (!dirInfo.mkpath(dirPath: imgOutDir))
889 en->location().fatal(QStringLiteral("Cannot create output directory '%1'").arg(a: imgOutDir));
890 Config::copyFile(location: en->location(), sourceFilePath: resolved_file.get_path(), userFriendlySourceFilePath: QFileInfo{resolved_file.get_query()}.fileName(), targetDirPath: imgOutDir);
891}
892
893// TODO: [multi-purpose-function-with-flag][generate-file-list]
894// Avoid the use of a boolean flag to dispatch to the correct
895// implementation trough branching.
896// We always have to process both images and files, such that we
897// should consider to remove the branching altogheter, performing both
898// operations in a single call.
899// Otherwise, if this turns out to be infeasible, complex or
900// possibly-confusing, consider extracting the processing code outside
901// the function and provide two higer-level dispathing functions for
902// files and images.
903
904/*!
905 This function is called when the documentation for an example is
906 being formatted. It outputs a list of files for the example, which
907 can be the example's source files or the list of images used by the
908 example. The images are copied into a subtree of
909 \c{...doc/html/images/used-in-examples/...}
910*/
911void Generator::generateFileList(const ExampleNode *en, CodeMarker *marker, bool images)
912{
913 Text text;
914 OpenedList openedList(OpenedList::Bullet);
915 QString tag;
916 QStringList paths;
917 Atom::AtomType atomType = Atom::ExampleFileLink;
918
919 if (images) {
920 paths = en->images();
921 tag = "Images:";
922 atomType = Atom::ExampleImageLink;
923 } else { // files
924 paths = en->files();
925 tag = "Files:";
926 }
927 std::sort(first: paths.begin(), last: paths.end(), comp: Generator::comparePaths);
928
929 text << Atom::ParaLeft << tag << Atom::ParaRight;
930 text << Atom(Atom::ListLeft, openedList.styleString());
931
932 for (const auto &path : std::as_const(t&: paths)) {
933 auto maybe_resolved_file{file_resolver.resolve(filename: path)};
934 if (!maybe_resolved_file) {
935 // TODO: [uncentralized-admonition][failed-resolve-file]
936 QString details = std::transform_reduce(
937 first: file_resolver.get_search_directories().cbegin(),
938 last: file_resolver.get_search_directories().cend(),
939 init: u"Searched directories:"_s,
940 binary_op: std::plus(),
941 unary_op: [](const DirectoryPath &directory_path) -> QString { return u' ' + directory_path.value(); }
942 );
943
944 en->location().warning(message: u"(Generator)Cannot find file to quote from: %1"_s.arg(a: path), details);
945
946 continue;
947 }
948
949 auto file{*maybe_resolved_file};
950 if (images) addImageToCopy(en, resolved_file: file);
951 else generateExampleFilePage(en, file, marker);
952
953 openedList.next();
954 text << Atom(Atom::ListItemNumber, openedList.numberString())
955 << Atom(Atom::ListItemLeft, openedList.styleString()) << Atom::ParaLeft
956 << Atom(atomType, file.get_query()) << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << file.get_query()
957 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << Atom::ParaRight
958 << Atom(Atom::ListItemRight, openedList.styleString());
959 }
960 text << Atom(Atom::ListRight, openedList.styleString());
961 if (!paths.isEmpty())
962 generateText(text, relative: en, marker);
963}
964
965/*!
966 Recursive writing of HTML files from the root \a node.
967 */
968void Generator::generateDocumentation(Node *node)
969{
970 if (!node->url().isNull())
971 return;
972 if (node->isIndexNode())
973 return;
974 if (node->isInternal() && !m_showInternal)
975 return;
976 if (node->isExternalPage())
977 return;
978
979 /*
980 Obtain a code marker for the source file.
981 */
982 CodeMarker *marker = CodeMarker::markerForFileName(fileName: node->location().filePath());
983
984 if (node->parent() != nullptr) {
985 if (node->isCollectionNode()) {
986 /*
987 A collection node collects: groups, C++ modules, or QML
988 modules. Testing for a CollectionNode must be done
989 before testing for a TextPageNode because a
990 CollectionNode is a PageNode at this point.
991
992 Don't output an HTML page for the collection node unless
993 the \group, \module, or \qmlmodule command was actually
994 seen by qdoc in the qdoc comment for the node.
995
996 A key prerequisite in this case is the call to
997 mergeCollections(cn). We must determine whether this
998 group, module or QML module has members in other
999 modules. We know at this point that cn's members list
1000 contains only members in the current module. Therefore,
1001 before outputting the page for cn, we must search for
1002 members of cn in the other modules and add them to the
1003 members list.
1004 */
1005 auto *cn = static_cast<CollectionNode *>(node);
1006 if (cn->wasSeen()) {
1007 m_qdb->mergeCollections(c: cn);
1008 beginSubPage(node, fileName: fileName(node));
1009 generateCollectionNode(cn, marker);
1010 endSubPage();
1011 } else if (cn->isGenericCollection()) {
1012 // Currently used only for the module's related orphans page
1013 // but can be generalized for other kinds of collections if
1014 // other use cases pop up.
1015 QString name = cn->name().toLower();
1016 name.replace(c: QChar(' '), after: QString("-"));
1017 QString filename =
1018 cn->tree()->physicalModuleName() + "-" + name + "." + fileExtension();
1019 beginSubPage(node, fileName: filename);
1020 generateGenericCollectionPage(cn, marker);
1021 endSubPage();
1022 }
1023 } else if (node->isTextPageNode()) {
1024 beginSubPage(node, fileName: fileName(node));
1025 generatePageNode(static_cast<PageNode *>(node), marker);
1026 endSubPage();
1027 } else if (node->isAggregate()) {
1028 if ((node->isClassNode() || node->isHeader() || node->isNamespace())
1029 && node->docMustBeGenerated()) {
1030 beginSubPage(node, fileName: fileName(node));
1031 generateCppReferencePage(static_cast<Aggregate *>(node), marker);
1032 endSubPage();
1033 } else if (node->isQmlType()) {
1034 beginSubPage(node, fileName: fileName(node));
1035 auto *qcn = static_cast<QmlTypeNode *>(node);
1036 generateQmlTypePage(qcn, marker);
1037 endSubPage();
1038 } else if (node->isProxyNode()) {
1039 beginSubPage(node, fileName: fileName(node));
1040 generateProxyPage(static_cast<Aggregate *>(node), marker);
1041 endSubPage();
1042 }
1043 }
1044 }
1045
1046 if (node->isAggregate()) {
1047 auto *aggregate = static_cast<Aggregate *>(node);
1048 const NodeList &children = aggregate->childNodes();
1049 for (auto *child : children) {
1050 if (child->isPageNode() && !child->isPrivate()) {
1051 generateDocumentation(node: child);
1052 } else if (!node->parent() && child->isInAPI() && !child->isRelatedNonmember()) {
1053 // Warn if there are documented non-page-generating nodes in the root namespace
1054 child->location().warning(message: u"No documentation generated for %1 '%2' in global scope."_s
1055 .arg(args: typeString(node: child), args: child->name()),
1056 details: u"Maybe you forgot to use the '\\relates' command?"_s);
1057 child->setStatus(Node::DontDocument);
1058 }
1059 }
1060 }
1061}
1062
1063void Generator::generateReimplementsClause(const FunctionNode *fn, CodeMarker *marker)
1064{
1065 if (fn->overridesThis().isEmpty() || !fn->parent()->isClassNode())
1066 return;
1067
1068 auto *cn = static_cast<ClassNode *>(fn->parent());
1069 const FunctionNode *overrides = cn->findOverriddenFunction(fn);
1070 if (overrides && !overrides->isPrivate() && !overrides->parent()->isPrivate()) {
1071 if (overrides->hasDoc()) {
1072 Text text;
1073 text << Atom::ParaLeft << "Reimplements: ";
1074 QString fullName =
1075 overrides->parent()->name()
1076 + "::" + overrides->signature(options: Node::SignaturePlain);
1077 appendFullName(text, apparentNode: overrides->parent(), fullName, actualNode: overrides);
1078 text << "." << Atom::ParaRight;
1079 generateText(text, relative: fn, marker);
1080 } else {
1081 fn->doc().location().warning(
1082 QStringLiteral("Illegal \\reimp; no documented virtual function for %1")
1083 .arg(a: overrides->plainSignature()));
1084 }
1085 return;
1086 }
1087 const PropertyNode *sameName = cn->findOverriddenProperty(fn);
1088 if (sameName && sameName->hasDoc()) {
1089 Text text;
1090 text << Atom::ParaLeft << "Reimplements an access function for property: ";
1091 QString fullName = sameName->parent()->name() + "::" + sameName->name();
1092 appendFullName(text, apparentNode: sameName->parent(), fullName, actualNode: sameName);
1093 text << "." << Atom::ParaRight;
1094 generateText(text, relative: fn, marker);
1095 }
1096}
1097
1098QString Generator::formatSince(const Node *node)
1099{
1100 QStringList since = node->since().split(sep: QLatin1Char(' '));
1101
1102 // If there is only one argument, assume it is the Qt version number.
1103 if (since.size() == 1)
1104 return "Qt " + since[0];
1105
1106 // Otherwise, use the original <project> <version> string.
1107 return node->since();
1108}
1109
1110/*!
1111 \internal
1112 Returns a string representing status information of a \a node.
1113
1114 If a status description is returned, it is one of:
1115 \list
1116 \li Custom status set explicitly in node's documentation using
1117 \c {\meta {status} {<description>}},
1118 \li 'Deprecated [since <version>]' (\\deprecated [<version>]),
1119 \li 'Preliminary' (\\preliminary), or
1120 \li The description adopted from associated module's state:
1121 \c {\modulestate {<description>}}.
1122 \endlist
1123
1124 Otherwise, returns \c std::nullopt.
1125*/
1126std::optional<QString> formatStatus(const Node *node, QDocDatabase *qdb)
1127{
1128 QString status;
1129
1130 if (const auto metaMap = node->doc().metaTagMap(); metaMap) {
1131 status = metaMap->value(key: "status");
1132 } else if (node->status() == Node::Deprecated) {
1133 status = u"Deprecated"_s;
1134 if (const auto since = node->deprecatedSince(); !since.isEmpty())
1135 status += " since %1"_L1.arg(args: since);
1136 } else if (node->status() == Node::Preliminary) {
1137 status = u"Preliminary"_s;
1138 } else if (const auto collection = qdb->getModuleNode(relative: node); collection) {
1139 status = collection->state();
1140 }
1141
1142 return status.isEmpty() ? std::nullopt : std::optional(status);
1143}
1144
1145void Generator::generateSince(const Node *node, CodeMarker *marker)
1146{
1147 if (!node->since().isEmpty()) {
1148 Text text;
1149 text << Atom::ParaLeft << "This " << typeString(node) << " was introduced in "
1150 << formatSince(node) << "." << Atom::ParaRight;
1151 generateText(text, relative: node, marker);
1152 }
1153}
1154
1155void Generator::generateNoexceptNote(const Node* node, CodeMarker* marker) {
1156 std::vector<const Node*> nodes;
1157 if (node->isSharedCommentNode()) {
1158 auto shared_node = static_cast<const SharedCommentNode*>(node);
1159 nodes.reserve(n: shared_node->collective().size());
1160 nodes.insert(position: nodes.begin(), first: shared_node->collective().begin(), last: shared_node->collective().end());
1161 } else nodes.push_back(x: node);
1162
1163 std::size_t counter{1};
1164 for (const Node* node : nodes) {
1165 if (node->isFunction(g: Node::CPP)) {
1166 if (auto exception_info = static_cast<const FunctionNode*>(node)->getNoexcept(); exception_info && !(*exception_info).isEmpty()) {
1167 Text text;
1168 text << Atom::NoteLeft
1169 << (nodes.size() > 1 ? QString::fromStdString(s: " ("s + std::to_string(val: counter) + ")"s) : QString::fromStdString(s: "This ") + typeString(node))
1170 << " does not throw any exception when " << "\"" << *exception_info << "\"" << " is true."
1171 << Atom::NoteRight;
1172 generateText(text, relative: node, marker);
1173 }
1174 }
1175
1176 ++counter;
1177 }
1178}
1179
1180void Generator::generateStatus(const Node *node, CodeMarker *marker)
1181{
1182 Text text;
1183
1184 switch (node->status()) {
1185 case Node::Active:
1186 // Output the module 'state' description if set.
1187 if (node->isModule() || node->isQmlModule()) {
1188 const QString &state = static_cast<const CollectionNode*>(node)->state();
1189 if (!state.isEmpty()) {
1190 text << Atom::ParaLeft << "This " << typeString(node) << " is in "
1191 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_ITALIC) << state
1192 << Atom(Atom::FormattingRight, ATOM_FORMATTING_ITALIC) << " state."
1193 << Atom::ParaRight;
1194 }
1195 }
1196 break;
1197 case Node::Preliminary:
1198 text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD) << "This "
1199 << typeString(node) << " is under development and is subject to change."
1200 << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD) << Atom::ParaRight;
1201 break;
1202 case Node::Deprecated:
1203 text << Atom::ParaLeft;
1204 if (node->isAggregate())
1205 text << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD);
1206 text << "This " << typeString(node) << " is deprecated";
1207 if (const QString &version = node->deprecatedSince(); !version.isEmpty())
1208 text << " since " << version;
1209 text << ". We strongly advise against using it in new code.";
1210 if (node->isAggregate())
1211 text << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD);
1212 text << Atom::ParaRight;
1213 break;
1214 case Node::Internal:
1215 default:
1216 break;
1217 }
1218 generateText(text, relative: node, marker);
1219}
1220
1221/*!
1222 Generates an addendum note of type \a type for \a node, using \a marker
1223 as the code marker.
1224*/
1225void Generator::generateAddendum(const Node *node, Addendum type, CodeMarker *marker,
1226 bool generateNote)
1227{
1228 Q_ASSERT(node && !node->name().isEmpty());
1229 Text text;
1230 text << Atom(Atom::DivLeft,
1231 "class=\"admonition %1\""_L1.arg(args: generateNote ? u"note"_s : u"auto"_s));
1232 text << Atom::ParaLeft;
1233
1234 if (generateNote) {
1235 text << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD)
1236 << "Note: " << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD);
1237 }
1238
1239 switch (type) {
1240 case Invokable:
1241 text << "This function can be invoked via the meta-object system and from QML. See "
1242 << Atom(Atom::AutoLink, "Q_INVOKABLE")
1243 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
1244 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << ".";
1245 break;
1246 case PrivateSignal:
1247 text << "This is a private signal. It can be used in signal connections "
1248 "but cannot be emitted by the user.";
1249 break;
1250 case QmlSignalHandler:
1251 {
1252 QString handler(node->name());
1253 qsizetype prefixLocation = handler.lastIndexOf(c: '.', from: -2) + 1;
1254 handler[prefixLocation] = handler[prefixLocation].toTitleCase();
1255 handler.insert(i: prefixLocation, s: QLatin1String("on"));
1256 text << "The corresponding handler is "
1257 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_TELETYPE) << handler
1258 << Atom(Atom::FormattingRight, ATOM_FORMATTING_TELETYPE) << ".";
1259 break;
1260 }
1261 case AssociatedProperties:
1262 {
1263 if (!node->isFunction())
1264 return;
1265 const auto *fn = static_cast<const FunctionNode *>(node);
1266 auto nodes = fn->associatedProperties();
1267 if (nodes.isEmpty())
1268 return;
1269 std::sort(first: nodes.begin(), last: nodes.end(), comp: Node::nodeNameLessThan);
1270 for (const auto *n : std::as_const(t&: nodes)) {
1271 QString msg;
1272 const auto *pn = static_cast<const PropertyNode *>(n);
1273 switch (pn->role(functionNode: fn)) {
1274 case PropertyNode::FunctionRole::Getter:
1275 msg = QStringLiteral("Getter function");
1276 break;
1277 case PropertyNode::FunctionRole::Setter:
1278 msg = QStringLiteral("Setter function");
1279 break;
1280 case PropertyNode::FunctionRole::Resetter:
1281 msg = QStringLiteral("Resetter function");
1282 break;
1283 case PropertyNode::FunctionRole::Notifier:
1284 msg = QStringLiteral("Notifier signal");
1285 break;
1286 default:
1287 continue;
1288 }
1289 text << msg << " for property " << Atom(Atom::Link, pn->name())
1290 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << pn->name()
1291 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << ". ";
1292 }
1293 break;
1294 }
1295 case BindableProperty:
1296 {
1297 text << "This property supports "
1298 << Atom(Atom::Link, "QProperty")
1299 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << "QProperty"
1300 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
1301 text << " bindings.";
1302 break;
1303 }
1304 default:
1305 return;
1306 }
1307
1308 text << Atom::ParaRight
1309 << Atom::DivRight;
1310 generateText(text, relative: node, marker);
1311}
1312
1313/*!
1314 Generate the documentation for \a relative. i.e. \a relative
1315 is the node that represents the entity where a qdoc comment
1316 was found, and \a text represents the qdoc comment.
1317 */
1318bool Generator::generateText(const Text &text, const Node *relative, CodeMarker *marker)
1319{
1320 bool result = false;
1321 if (text.firstAtom() != nullptr) {
1322 int numAtoms = 0;
1323 initializeTextOutput();
1324 generateAtomList(atom: text.firstAtom(), relative, marker, generate: true, numAtoms);
1325 result = true;
1326 }
1327 return result;
1328}
1329
1330/*
1331 The node is an aggregate, typically a class node, which has
1332 a threadsafeness level. This function checks all the children
1333 of the node to see if they are exceptions to the node's
1334 threadsafeness. If there are any exceptions, the exceptions
1335 are added to the appropriate set (reentrant, threadsafe, and
1336 nonreentrant, and true is returned. If there are no exceptions,
1337 the three node lists remain empty and false is returned.
1338 */
1339bool Generator::hasExceptions(const Node *node, NodeList &reentrant, NodeList &threadsafe,
1340 NodeList &nonreentrant)
1341{
1342 bool result = false;
1343 Node::ThreadSafeness ts = node->threadSafeness();
1344 const NodeList &children = static_cast<const Aggregate *>(node)->childNodes();
1345 for (auto child : children) {
1346 if (!child->isDeprecated()) {
1347 switch (child->threadSafeness()) {
1348 case Node::Reentrant:
1349 reentrant.append(t: child);
1350 if (ts == Node::ThreadSafe)
1351 result = true;
1352 break;
1353 case Node::ThreadSafe:
1354 threadsafe.append(t: child);
1355 if (ts == Node::Reentrant)
1356 result = true;
1357 break;
1358 case Node::NonReentrant:
1359 nonreentrant.append(t: child);
1360 result = true;
1361 break;
1362 default:
1363 break;
1364 }
1365 }
1366 }
1367 return result;
1368}
1369
1370static void startNote(Text &text)
1371{
1372 text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD)
1373 << "Note:" << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD) << " ";
1374}
1375
1376/*!
1377 Generates text that explains how threadsafe and/or reentrant
1378 \a node is.
1379 */
1380void Generator::generateThreadSafeness(const Node *node, CodeMarker *marker)
1381{
1382 Text text, rlink, tlink;
1383 NodeList reentrant;
1384 NodeList threadsafe;
1385 NodeList nonreentrant;
1386 Node::ThreadSafeness ts = node->threadSafeness();
1387 bool exceptions = false;
1388
1389 rlink << Atom(Atom::Link, "reentrant") << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
1390 << "reentrant" << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
1391
1392 tlink << Atom(Atom::Link, "thread-safe") << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
1393 << "thread-safe" << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
1394
1395 switch (ts) {
1396 case Node::UnspecifiedSafeness:
1397 break;
1398 case Node::NonReentrant:
1399 text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD)
1400 << "Warning:" << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD) << " This "
1401 << typeString(node) << " is not " << rlink << "." << Atom::ParaRight;
1402 break;
1403 case Node::Reentrant:
1404 case Node::ThreadSafe:
1405 startNote(text);
1406 if (node->isAggregate()) {
1407 exceptions = hasExceptions(node, reentrant, threadsafe, nonreentrant);
1408 text << "All functions in this " << typeString(node) << " are ";
1409 if (ts == Node::ThreadSafe)
1410 text << tlink;
1411 else
1412 text << rlink;
1413
1414 if (!exceptions || (ts == Node::Reentrant && !threadsafe.isEmpty()))
1415 text << ".";
1416 else
1417 text << " with the following exceptions:";
1418 } else {
1419 text << "This " << typeString(node) << " is ";
1420 if (ts == Node::ThreadSafe)
1421 text << tlink;
1422 else
1423 text << rlink;
1424 text << ".";
1425 }
1426 text << Atom::ParaRight;
1427 break;
1428 default:
1429 break;
1430 }
1431 generateText(text, relative: node, marker);
1432
1433 if (exceptions) {
1434 text.clear();
1435 if (ts == Node::Reentrant) {
1436 if (!nonreentrant.isEmpty()) {
1437 startNote(text);
1438 text << "These functions are not " << rlink << ":" << Atom::ParaRight;
1439 signatureList(nodes: nonreentrant, relative: node, marker);
1440 }
1441 if (!threadsafe.isEmpty()) {
1442 text.clear();
1443 startNote(text);
1444 text << "These functions are also " << tlink << ":" << Atom::ParaRight;
1445 generateText(text, relative: node, marker);
1446 signatureList(nodes: threadsafe, relative: node, marker);
1447 }
1448 } else { // thread-safe
1449 if (!reentrant.isEmpty()) {
1450 startNote(text);
1451 text << "These functions are only " << rlink << ":" << Atom::ParaRight;
1452 signatureList(nodes: reentrant, relative: node, marker);
1453 }
1454 if (!nonreentrant.isEmpty()) {
1455 text.clear();
1456 startNote(text);
1457 text << "These functions are not " << rlink << ":" << Atom::ParaRight;
1458 signatureList(nodes: nonreentrant, relative: node, marker);
1459 }
1460 }
1461 }
1462}
1463
1464/*!
1465 Returns the string containing an example code of the input node,
1466 if it is an overloaded signal. Otherwise, returns an empty string.
1467 */
1468QString Generator::getOverloadedSignalCode(const Node *node)
1469{
1470 if (!node->isFunction())
1471 return QString();
1472 const auto func = static_cast<const FunctionNode *>(node);
1473 if (!func->isSignal() || !func->hasOverloads())
1474 return QString();
1475
1476 // Compute a friendly name for the object of that instance.
1477 // e.g: "QAbstractSocket" -> "abstractSocket"
1478 QString objectName = node->parent()->name();
1479 if (objectName.size() >= 2) {
1480 if (objectName[0] == 'Q')
1481 objectName = objectName.mid(position: 1);
1482 objectName[0] = objectName[0].toLower();
1483 }
1484
1485 // We have an overloaded signal, show an example. Note, for const
1486 // overloaded signals, one should use Q{Const,NonConst}Overload, but
1487 // it is very unlikely that we will ever have public API overloading
1488 // signals by const.
1489 QString code = "connect(" + objectName + ", QOverload<";
1490 code += func->parameters().generateTypeList();
1491 code += ">::of(&" + func->parent()->name() + "::" + func->name() + "),\n [=](";
1492 code += func->parameters().generateTypeAndNameList();
1493 code += "){ /* ... */ });";
1494
1495 return code;
1496}
1497
1498/*!
1499 If the node is an overloaded signal, add a node with an example on how to connect to it
1500 */
1501void Generator::generateOverloadedSignal(const Node *node, CodeMarker *marker)
1502{
1503 QString code = getOverloadedSignalCode(node);
1504 if (code.isEmpty())
1505 return;
1506
1507 Text text;
1508 text << Atom::ParaLeft << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD)
1509 << "Note:" << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD) << " Signal "
1510 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_ITALIC) << node->name()
1511 << Atom(Atom::FormattingRight, ATOM_FORMATTING_ITALIC)
1512 << " is overloaded in this class. "
1513 "To connect to this signal by using the function pointer syntax, Qt "
1514 "provides a convenient helper for obtaining the function pointer as "
1515 "shown in this example:"
1516 << Atom(Atom::Code, marker->markedUpCode(code, node, node->location()));
1517
1518 generateText(text, relative: node, marker);
1519}
1520
1521/*!
1522 Traverses the database recursively to generate all the documentation.
1523 */
1524void Generator::generateDocs()
1525{
1526 s_currentGenerator = this;
1527 generateDocumentation(node: m_qdb->primaryTreeRoot());
1528}
1529
1530Generator *Generator::generatorForFormat(const QString &format)
1531{
1532 for (const auto &generator : std::as_const(t&: s_generators)) {
1533 if (generator->format() == format)
1534 return generator;
1535 }
1536 return nullptr;
1537}
1538
1539/*!
1540 Looks up the tag \a t in the map of metadata values for the
1541 current topic in \a inner. If values for the tag are found,
1542 they are returned in a string list.
1543
1544 \note If \a t is found in the metadata map, all the pairs
1545 having the key \a t are erased. i.e. Once you call this
1546 function for a particular \a t, you consume \a t.
1547 */
1548QStringList Generator::getMetadataElements(const Aggregate *inner, const QString &t)
1549{
1550 QStringList result;
1551 QStringMultiMap *metaTagMap = inner->doc().metaTagMap();
1552 if (metaTagMap)
1553 result = metaTagMap->values(key: t);
1554 if (!result.isEmpty())
1555 metaTagMap->remove(key: t);
1556 return result;
1557}
1558
1559QString Generator::indent(int level, const QString &markedCode)
1560{
1561 if (level == 0)
1562 return markedCode;
1563
1564 QString t;
1565 int column = 0;
1566
1567 int i = 0;
1568 while (i < markedCode.size()) {
1569 if (markedCode.at(i) == QLatin1Char('\n')) {
1570 column = 0;
1571 } else {
1572 if (column == 0) {
1573 for (int j = 0; j < level; j++)
1574 t += QLatin1Char(' ');
1575 }
1576 column++;
1577 }
1578 t += markedCode.at(i: i++);
1579 }
1580 return t;
1581}
1582
1583void Generator::initialize()
1584{
1585 Config &config = Config::instance();
1586 s_outputFormats = config.getOutputFormats();
1587 s_redirectDocumentationToDevNull = config.get(CONFIG_REDIRECTDOCUMENTATIONTODEVNULL).asBool();
1588
1589 for (auto &g : s_generators) {
1590 if (s_outputFormats.contains(value: g->format())) {
1591 s_currentGenerator = g;
1592 g->initializeGenerator();
1593 }
1594 }
1595
1596 const auto &configFormatting = config.subVars(CONFIG_FORMATTING);
1597 for (const auto &n : configFormatting) {
1598 QString formattingDotName = CONFIG_FORMATTING + Config::dot + n;
1599 const auto &formattingDotNames = config.subVars(var: formattingDotName);
1600 for (const auto &f : formattingDotNames) {
1601 const auto &configVar = config.get(var: formattingDotName + Config::dot + f);
1602 QString def{configVar.asString()};
1603 if (!def.isEmpty()) {
1604 int numParams = Config::numParams(value: def);
1605 int numOccs = def.count(s: "\1");
1606 if (numParams != 1) {
1607 configVar.location().warning(QStringLiteral("Formatting '%1' must "
1608 "have exactly one "
1609 "parameter (found %2)")
1610 .arg(a: n, fieldWidth: numParams));
1611 } else if (numOccs > 1) {
1612 configVar.location().fatal(QStringLiteral("Formatting '%1' must "
1613 "contain exactly one "
1614 "occurrence of '\\1' "
1615 "(found %2)")
1616 .arg(a: n, fieldWidth: numOccs));
1617 } else {
1618 int paramPos = def.indexOf(s: "\1");
1619 s_fmtLeftMaps[f].insert(key: n, value: def.left(n: paramPos));
1620 s_fmtRightMaps[f].insert(key: n, value: def.mid(position: paramPos + 1));
1621 }
1622 }
1623 }
1624 }
1625
1626 s_project = config.get(CONFIG_PROJECT).asString();
1627 s_outDir = config.getOutputDir();
1628 s_outSubdir = s_outDir.mid(position: s_outDir.lastIndexOf(c: '/') + 1);
1629
1630 s_outputPrefixes.clear();
1631 QStringList items{config.get(CONFIG_OUTPUTPREFIXES).asStringList()};
1632 if (!items.isEmpty()) {
1633 for (const auto &prefix : items)
1634 s_outputPrefixes[prefix] =
1635 config.get(CONFIG_OUTPUTPREFIXES + Config::dot + prefix).asString();
1636 } else {
1637 s_outputPrefixes[QLatin1String("QML")] = QLatin1String("qml-");
1638 }
1639
1640 s_outputSuffixes.clear();
1641 for (const auto &suffix : config.get(CONFIG_OUTPUTSUFFIXES).asStringList())
1642 s_outputSuffixes[suffix] = config.get(CONFIG_OUTPUTSUFFIXES
1643 + Config::dot + suffix).asString();
1644
1645 s_noLinkErrors = config.get(CONFIG_NOLINKERRORS).asBool();
1646 s_autolinkErrors = config.get(CONFIG_AUTOLINKERRORS).asBool();
1647}
1648
1649/*!
1650 Creates template-specific subdirs (e.g. /styles and /scripts for HTML)
1651 and copies the files to them.
1652 */
1653void Generator::copyTemplateFiles(const QString &configVar, const QString &subDir)
1654{
1655 // TODO: [resolving-files-unlinked-to-doc]
1656 // This is another case of resolving files, albeit it doesn't use Doc::resolveFile.
1657 // While it may be left out of a first iteration of the file
1658 // resolution logic, it should later be integrated into it.
1659 // This should come naturally when the output directory logic is
1660 // extracted and copying a file should require a validated
1661 // intermediate format.
1662 // Do note that what is done here is a bit different from the
1663 // resolve file routine that is done for other user-given paths.
1664 // Thas is, the paths will always be absolute and not relative as
1665 // they are resolved from the configuration.
1666 // Ideally, this could be solved in the configuration already,
1667 // together with the other configuration resolution processes that
1668 // do not abide by the same constraints that, for example, snippet
1669 // resolution uses.
1670 Config &config = Config::instance();
1671 QStringList files = config.getCanonicalPathList(var: configVar, flags: Config::Validate);
1672 const auto &loc = config.get(var: configVar).location();
1673 if (!files.isEmpty()) {
1674 QDir dirInfo;
1675 // TODO: [uncentralized-output-directory-structure]
1676 // As with other places in the generation pass, the details of
1677 // where something is saved in the output directory are spread
1678 // to whichever part of the generation does the saving.
1679 // It is hence complex to build a model of how an output
1680 // directory looks like, as the knowledge has no specific
1681 // entry point or chain-path that can be followed in full.
1682 // Each of those operations should be centralized in a system
1683 // that uniquely knows what the format of the output-directory
1684 // is and how to perform operations on it.
1685 // Later, move this operation to that centralized system.
1686 QString templateDir = s_outDir + QLatin1Char('/') + subDir;
1687 if (!dirInfo.exists(name: templateDir) && !dirInfo.mkdir(dirName: templateDir)) {
1688 // TODO: [uncentralized-admonition]
1689 loc.fatal(QStringLiteral("Cannot create %1 directory '%2'").arg(args: subDir, args&: templateDir));
1690 } else {
1691 for (const auto &file : files) {
1692 if (!file.isEmpty())
1693 Config::copyFile(location: loc, sourceFilePath: file, userFriendlySourceFilePath: file, targetDirPath: templateDir);
1694 }
1695 }
1696 }
1697}
1698
1699/*!
1700 Reads format-specific variables from config, sets output
1701 (sub)directories, creates them on the filesystem and copies the
1702 template-specific files.
1703 */
1704void Generator::initializeFormat()
1705{
1706 Config &config = Config::instance();
1707 s_outFileNames.clear();
1708 s_useOutputSubdirs = true;
1709 if (config.get(var: format() + Config::dot + "nosubdirs").asBool())
1710 resetUseOutputSubdirs();
1711
1712 if (s_outputFormats.isEmpty())
1713 return;
1714
1715 s_outDir = config.getOutputDir(format: format());
1716 if (s_outDir.isEmpty()) {
1717 Location().fatal(QStringLiteral("No output directory specified in "
1718 "configuration file or on the command line"));
1719 } else {
1720 s_outSubdir = s_outDir.mid(position: s_outDir.lastIndexOf(c: '/') + 1);
1721 }
1722
1723 QDir outputDir(s_outDir);
1724 if (outputDir.exists()) {
1725 if (!config.generating() && Generator::useOutputSubdirs()) {
1726 if (!outputDir.isEmpty())
1727 Location().error(QStringLiteral("Output directory '%1' exists but is not empty")
1728 .arg(a: s_outDir));
1729 }
1730 } else if (!outputDir.mkpath(QStringLiteral("."))) {
1731 Location().fatal(QStringLiteral("Cannot create output directory '%1'").arg(a: s_outDir));
1732 }
1733
1734 // Output directory exists, which is enough for prepare phase.
1735 if (config.preparing())
1736 return;
1737
1738 const QLatin1String imagesDir("images");
1739 if (!outputDir.exists(name: imagesDir) && !outputDir.mkdir(dirName: imagesDir))
1740 Location().fatal(QStringLiteral("Cannot create images directory '%1'").arg(a: outputDir.filePath(fileName: imagesDir)));
1741
1742 copyTemplateFiles(configVar: format() + Config::dot + CONFIG_STYLESHEETS, subDir: "style");
1743 copyTemplateFiles(configVar: format() + Config::dot + CONFIG_SCRIPTS, subDir: "scripts");
1744 copyTemplateFiles(configVar: format() + Config::dot + CONFIG_EXTRAIMAGES, subDir: "images");
1745
1746 // Use a format-specific .quotinginformation if defined, otherwise a global value
1747 if (config.subVars(var: format()).contains(CONFIG_QUOTINGINFORMATION))
1748 m_quoting = config.get(var: format() + Config::dot + CONFIG_QUOTINGINFORMATION).asBool();
1749 else
1750 m_quoting = config.get(CONFIG_QUOTINGINFORMATION).asBool();
1751}
1752
1753/*!
1754 Updates the generator's m_showInternal from the Config.
1755 */
1756void Generator::initializeGenerator()
1757{
1758 m_showInternal = Config::instance().showInternal();
1759}
1760
1761bool Generator::matchAhead(const Atom *atom, Atom::AtomType expectedAtomType)
1762{
1763 return atom->next() && atom->next()->type() == expectedAtomType;
1764}
1765
1766/*!
1767 Used for writing to the current output stream. Returns a
1768 reference to the current output stream, which is then used
1769 with the \c {<<} operator for writing.
1770 */
1771QTextStream &Generator::out()
1772{
1773 return *outStreamStack.top();
1774}
1775
1776QString Generator::outFileName()
1777{
1778 return QFileInfo(static_cast<QFile *>(out().device())->fileName()).fileName();
1779}
1780
1781QString Generator::outputPrefix(const Node *node)
1782{
1783 // Prefix is applied to QML types
1784 if (node->isQmlType())
1785 return s_outputPrefixes[QLatin1String("QML")];
1786
1787 return QString();
1788}
1789
1790QString Generator::outputSuffix(const Node *node)
1791{
1792 // Suffix is applied to QML types, as
1793 // well as module pages.
1794 if (node->isQmlModule() || node->isQmlType())
1795 return s_outputSuffixes[QLatin1String("QML")];
1796
1797 return QString();
1798}
1799
1800bool Generator::parseArg(const QString &src, const QString &tag, int *pos, int n,
1801 QStringView *contents, QStringView *par1)
1802{
1803#define SKIP_CHAR(c) \
1804 if (i >= n || src[i] != c) \
1805 return false; \
1806 ++i;
1807
1808#define SKIP_SPACE \
1809 while (i < n && src[i] == ' ') \
1810 ++i;
1811
1812 qsizetype i = *pos;
1813 qsizetype j {};
1814
1815 // assume "<@" has been parsed outside
1816 // SKIP_CHAR('<');
1817 // SKIP_CHAR('@');
1818
1819 if (tag != QStringView(src).mid(pos: i, n: tag.size())) {
1820 return false;
1821 }
1822
1823 // skip tag
1824 i += tag.size();
1825
1826 // parse stuff like: linkTag("(<@link node=\"([^\"]+)\">).*(</@link>)");
1827 if (par1) {
1828 SKIP_SPACE;
1829 // read parameter name
1830 j = i;
1831 while (i < n && src[i].isLetter())
1832 ++i;
1833 if (src[i] == '=') {
1834 SKIP_CHAR('=');
1835 SKIP_CHAR('"');
1836 // skip parameter name
1837 j = i;
1838 while (i < n && src[i] != '"')
1839 ++i;
1840 *par1 = QStringView(src).mid(pos: j, n: i - j);
1841 SKIP_CHAR('"');
1842 SKIP_SPACE;
1843 }
1844 }
1845 SKIP_SPACE;
1846 SKIP_CHAR('>');
1847
1848 // find contents up to closing "</@tag>
1849 j = i;
1850 for (; true; ++i) {
1851 if (i + 4 + tag.size() > n)
1852 return false;
1853 if (src[i] != '<')
1854 continue;
1855 if (src[i + 1] != '/')
1856 continue;
1857 if (src[i + 2] != '@')
1858 continue;
1859 if (tag != QStringView(src).mid(pos: i + 3, n: tag.size()))
1860 continue;
1861 if (src[i + 3 + tag.size()] != '>')
1862 continue;
1863 break;
1864 }
1865
1866 *contents = QStringView(src).mid(pos: j, n: i - j);
1867
1868 i += tag.size() + 4;
1869
1870 *pos = i;
1871 return true;
1872#undef SKIP_CHAR
1873#undef SKIP_SPACE
1874}
1875
1876QString Generator::plainCode(const QString &markedCode)
1877{
1878 QString t = markedCode;
1879 t.replace(re: tag, after: QString());
1880 t.replace(before: quot, after: QLatin1String("\""));
1881 t.replace(before: gt, after: QLatin1String(">"));
1882 t.replace(before: lt, after: QLatin1String("<"));
1883 t.replace(before: amp, after: QLatin1String("&"));
1884 return t;
1885}
1886
1887int Generator::skipAtoms(const Atom *atom, Atom::AtomType type) const
1888{
1889 int skipAhead = 0;
1890 atom = atom->next();
1891 while (atom && atom->type() != type) {
1892 skipAhead++;
1893 atom = atom->next();
1894 }
1895 return skipAhead;
1896}
1897
1898/*!
1899 Resets the variables used during text output.
1900 */
1901void Generator::initializeTextOutput()
1902{
1903 m_inLink = false;
1904 m_inContents = false;
1905 m_inSectionHeading = false;
1906 m_inTableHeader = false;
1907 m_numTableRows = 0;
1908 m_threeColumnEnumValueTable = true;
1909 m_link.clear();
1910 m_sectionNumber.clear();
1911}
1912
1913void Generator::supplementAlsoList(const Node *node, QList<Text> &alsoList)
1914{
1915 if (node->isFunction() && !node->isMacro()) {
1916 const auto fn = static_cast<const FunctionNode *>(node);
1917 if (fn->overloadNumber() == 0) {
1918 QString alternateName;
1919 const FunctionNode *alternateFunc = nullptr;
1920
1921 if (fn->name().startsWith(s: "set") && fn->name().size() >= 4) {
1922 alternateName = fn->name()[3].toLower();
1923 alternateName += fn->name().mid(position: 4);
1924 alternateFunc = fn->parent()->findFunctionChild(name: alternateName, parameters: QString());
1925
1926 if (!alternateFunc) {
1927 alternateName = "is" + fn->name().mid(position: 3);
1928 alternateFunc = fn->parent()->findFunctionChild(name: alternateName, parameters: QString());
1929 if (!alternateFunc) {
1930 alternateName = "has" + fn->name().mid(position: 3);
1931 alternateFunc = fn->parent()->findFunctionChild(name: alternateName, parameters: QString());
1932 }
1933 }
1934 } else if (!fn->name().isEmpty()) {
1935 alternateName = "set";
1936 alternateName += fn->name()[0].toUpper();
1937 alternateName += fn->name().mid(position: 1);
1938 alternateFunc = fn->parent()->findFunctionChild(name: alternateName, parameters: QString());
1939 }
1940
1941 if (alternateFunc && alternateFunc->access() != Access::Private) {
1942 int i;
1943 for (i = 0; i < alsoList.size(); ++i) {
1944 if (alsoList.at(i).toString().contains(s: alternateName))
1945 break;
1946 }
1947
1948 if (i == alsoList.size()) {
1949 if (alternateFunc->isDeprecated() && !fn->isDeprecated())
1950 return;
1951 alternateName += "()";
1952
1953 Text also;
1954 also << Atom(Atom::Link, alternateName)
1955 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << alternateName
1956 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
1957 alsoList.prepend(t: also);
1958 }
1959 }
1960 }
1961 }
1962}
1963
1964void Generator::terminate()
1965{
1966 for (const auto &generator : std::as_const(t&: s_generators)) {
1967 if (s_outputFormats.contains(value: generator->format()))
1968 generator->terminateGenerator();
1969 }
1970
1971 // REMARK: Generators currently, due to recent changes and the
1972 // transitive nature of the current codebase, receive some of
1973 // their dependencies in the constructor and some of them in their
1974 // initialize-terminate lifetime.
1975 // This means that generators need to be constructed and
1976 // destructed between usages such that if multiple usages are
1977 // required, the generators present in the list will have been
1978 // destroyed by then such that accessing them would be an error.
1979 // The current codebase calls initialize and the correspective
1980 // terminate with the same scope as the lifetime of the
1981 // generators.
1982 // Then, clearing the list ensures that, if another generator
1983 // execution is needed, the stale generators will not be removed
1984 // as to be replaced by newly constructed ones.
1985 // Do note that it is not clear that this will happen for any call
1986 // in Qt's documentation and this should work only because of the
1987 // form of the current codebase and the scoping of the
1988 // initialize-terminate calls. As such, this should be considered
1989 // a patchwork that may or may not be doing anything and that may
1990 // break due to changes in other parts of the codebase.
1991 //
1992 // This is still to be considered temporary as the whole
1993 // initialize-terminate idiom must be removed from the codebase.
1994 s_generators.clear();
1995
1996 s_fmtLeftMaps.clear();
1997 s_fmtRightMaps.clear();
1998 s_outDir.clear();
1999}
2000
2001void Generator::terminateGenerator() {}
2002
2003/*!
2004 Trims trailing whitespace off the \a string and returns
2005 the trimmed string.
2006 */
2007QString Generator::trimmedTrailing(const QString &string, const QString &prefix,
2008 const QString &suffix)
2009{
2010 QString trimmed = string;
2011 while (trimmed.size() > 0 && trimmed[trimmed.size() - 1].isSpace())
2012 trimmed.truncate(pos: trimmed.size() - 1);
2013
2014 trimmed.append(s: suffix);
2015 trimmed.prepend(s: prefix);
2016 return trimmed;
2017}
2018
2019QString Generator::typeString(const Node *node)
2020{
2021 switch (node->nodeType()) {
2022 case Node::Namespace:
2023 return "namespace";
2024 case Node::Class:
2025 return "class";
2026 case Node::Struct:
2027 return "struct";
2028 case Node::Union:
2029 return "union";
2030 case Node::QmlType:
2031 case Node::QmlValueType:
2032 return "type";
2033 case Node::Page:
2034 return "documentation";
2035 case Node::Enum:
2036 return "enum";
2037 case Node::Typedef:
2038 case Node::TypeAlias:
2039 return "typedef";
2040 case Node::Function: {
2041 const auto fn = static_cast<const FunctionNode *>(node);
2042 switch (fn->metaness()) {
2043 case FunctionNode::QmlSignal:
2044 return "signal";
2045 case FunctionNode::QmlSignalHandler:
2046 return "signal handler";
2047 case FunctionNode::QmlMethod:
2048 return "method";
2049 case FunctionNode::MacroWithParams:
2050 case FunctionNode::MacroWithoutParams:
2051 return "macro";
2052 default:
2053 break;
2054 }
2055 return "function";
2056 }
2057 case Node::Property:
2058 case Node::QmlProperty:
2059 return "property";
2060 case Node::Module:
2061 case Node::QmlModule:
2062 return "module";
2063 case Node::SharedComment: {
2064 const auto &collective = static_cast<const SharedCommentNode *>(node)->collective();
2065 return collective.first()->nodeTypeString();
2066 }
2067 default:
2068 return "documentation";
2069 }
2070}
2071
2072void Generator::unknownAtom(const Atom *atom)
2073{
2074 Location::internalError(QStringLiteral("unknown atom type '%1' in %2 generator")
2075 .arg(args: atom->typeString(), args: format()));
2076}
2077
2078QT_END_NAMESPACE
2079

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