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 "config.h"
5#include "utilities.h"
6
7#include <QtCore/qdir.h>
8#include <QtCore/qfile.h>
9#include <QtCore/qtemporaryfile.h>
10#include <QtCore/qtextstream.h>
11#include <QtCore/qvariant.h>
12#include <QtCore/qregularexpression.h>
13
14QT_BEGIN_NAMESPACE
15
16QString ConfigStrings::AUTOLINKERRORS = QStringLiteral("autolinkerrors");
17QString ConfigStrings::BUILDVERSION = QStringLiteral("buildversion");
18QString ConfigStrings::CODEINDENT = QStringLiteral("codeindent");
19QString ConfigStrings::CODEPREFIX = QStringLiteral("codeprefix");
20QString ConfigStrings::CODESUFFIX = QStringLiteral("codesuffix");
21QString ConfigStrings::CPPCLASSESPAGE = QStringLiteral("cppclassespage");
22QString ConfigStrings::CPPCLASSESTITLE = QStringLiteral("cppclassestitle");
23QString ConfigStrings::DEFINES = QStringLiteral("defines");
24QString ConfigStrings::DEPENDS = QStringLiteral("depends");
25QString ConfigStrings::DESCRIPTION = QStringLiteral("description");
26QString ConfigStrings::DOCBOOKEXTENSIONS = QStringLiteral("usedocbookextensions");
27QString ConfigStrings::DOCUMENTATIONINHEADERS = QStringLiteral("documentationinheaders");
28QString ConfigStrings::ENDHEADER = QStringLiteral("endheader");
29QString ConfigStrings::EXAMPLEDIRS = QStringLiteral("exampledirs");
30QString ConfigStrings::EXAMPLES = QStringLiteral("examples");
31QString ConfigStrings::EXAMPLESINSTALLPATH = QStringLiteral("examplesinstallpath");
32QString ConfigStrings::EXCLUDEDIRS = QStringLiteral("excludedirs");
33QString ConfigStrings::EXCLUDEFILES = QStringLiteral("excludefiles");
34QString ConfigStrings::EXTRAIMAGES = QStringLiteral("extraimages");
35QString ConfigStrings::FALSEHOODS = QStringLiteral("falsehoods");
36QString ConfigStrings::FORMATTING = QStringLiteral("formatting");
37QString ConfigStrings::HEADERDIRS = QStringLiteral("headerdirs");
38QString ConfigStrings::HEADERS = QStringLiteral("headers");
39QString ConfigStrings::HEADERSCRIPTS = QStringLiteral("headerscripts");
40QString ConfigStrings::HEADERSTYLES = QStringLiteral("headerstyles");
41QString ConfigStrings::HOMEPAGE = QStringLiteral("homepage");
42QString ConfigStrings::HOMETITLE = QStringLiteral("hometitle");
43QString ConfigStrings::IGNOREDIRECTIVES = QStringLiteral("ignoredirectives");
44QString ConfigStrings::IGNORESINCE = QStringLiteral("ignoresince");
45QString ConfigStrings::IGNORETOKENS = QStringLiteral("ignoretokens");
46QString ConfigStrings::IGNOREWORDS = QStringLiteral("ignorewords");
47QString ConfigStrings::IMAGEDIRS = QStringLiteral("imagedirs");
48QString ConfigStrings::INCLUDEPATHS = QStringLiteral("includepaths");
49QString ConfigStrings::INCLUSIVE = QStringLiteral("inclusive");
50QString ConfigStrings::INDEXES = QStringLiteral("indexes");
51QString ConfigStrings::LANDINGPAGE = QStringLiteral("landingpage");
52QString ConfigStrings::LANDINGTITLE = QStringLiteral("landingtitle");
53QString ConfigStrings::LANGUAGE = QStringLiteral("language");
54QString ConfigStrings::LOCATIONINFO = QStringLiteral("locationinfo");
55QString ConfigStrings::LOGPROGRESS = QStringLiteral("logprogress");
56QString ConfigStrings::MACRO = QStringLiteral("macro");
57QString ConfigStrings::MANIFESTMETA = QStringLiteral("manifestmeta");
58QString ConfigStrings::MODULEHEADER = QStringLiteral("moduleheader");
59QString ConfigStrings::NATURALLANGUAGE = QStringLiteral("naturallanguage");
60QString ConfigStrings::NAVIGATION = QStringLiteral("navigation");
61QString ConfigStrings::NOLINKERRORS = QStringLiteral("nolinkerrors");
62QString ConfigStrings::OUTPUTDIR = QStringLiteral("outputdir");
63QString ConfigStrings::OUTPUTFORMATS = QStringLiteral("outputformats");
64QString ConfigStrings::OUTPUTPREFIXES = QStringLiteral("outputprefixes");
65QString ConfigStrings::OUTPUTSUFFIXES = QStringLiteral("outputsuffixes");
66QString ConfigStrings::PRODUCTNAME QStringLiteral("productname");
67QString ConfigStrings::PROJECT = QStringLiteral("project");
68QString ConfigStrings::REDIRECTDOCUMENTATIONTODEVNULL =
69 QStringLiteral("redirectdocumentationtodevnull");
70QString ConfigStrings::REPORTMISSINGALTTEXTFORIMAGES =
71 QStringLiteral("reportmissingalttextforimages");
72QString ConfigStrings::QHP = QStringLiteral("qhp");
73QString ConfigStrings::QUOTINGINFORMATION = QStringLiteral("quotinginformation");
74QString ConfigStrings::ROOTDIR = QStringLiteral("rootdir");
75QString ConfigStrings::SCRIPTS = QStringLiteral("scripts");
76QString ConfigStrings::SHOWINTERNAL = QStringLiteral("showinternal");
77QString ConfigStrings::SINGLEEXEC = QStringLiteral("singleexec");
78QString ConfigStrings::SOURCEDIRS = QStringLiteral("sourcedirs");
79QString ConfigStrings::SOURCEENCODING = QStringLiteral("sourceencoding");
80QString ConfigStrings::SOURCES = QStringLiteral("sources");
81QString ConfigStrings::SPURIOUS = QStringLiteral("spurious");
82QString ConfigStrings::STYLESHEETS = QStringLiteral("stylesheets");
83QString ConfigStrings::SYNTAXHIGHLIGHTING = QStringLiteral("syntaxhighlighting");
84QString ConfigStrings::TABSIZE = QStringLiteral("tabsize");
85QString ConfigStrings::TAGFILE = QStringLiteral("tagfile");
86QString ConfigStrings::TIMESTAMPS = QStringLiteral("timestamps");
87QString ConfigStrings::TOCTITLES = QStringLiteral("toctitles");
88QString ConfigStrings::TRADEMARKSPAGE = QStringLiteral("trademarkspage");
89QString ConfigStrings::URL = QStringLiteral("url");
90QString ConfigStrings::USEALTTEXTASTITLE = QStringLiteral("usealttextastitle");
91QString ConfigStrings::VERSION = QStringLiteral("version");
92QString ConfigStrings::VERSIONSYM = QStringLiteral("versionsym");
93QString ConfigStrings::FILEEXTENSIONS = QStringLiteral("fileextensions");
94QString ConfigStrings::IMAGEEXTENSIONS = QStringLiteral("imageextensions");
95QString ConfigStrings::QMLTYPESPAGE = QStringLiteral("qmltypespage");
96QString ConfigStrings::QMLTYPESTITLE = QStringLiteral("qmltypestitle");
97QString ConfigStrings::WARNABOUTMISSINGIMAGES = QStringLiteral("warnaboutmissingimages");
98QString ConfigStrings::WARNABOUTMISSINGPROJECTFILES = QStringLiteral("warnaboutmissingprojectfiles");
99QString ConfigStrings::WARNINGLIMIT = QStringLiteral("warninglimit");
100
101/*!
102 An entry in a stack, where each entry is a list
103 of string values.
104 */
105class MetaStackEntry
106{
107public:
108 void open();
109 void close();
110
111 QStringList accum;
112 QStringList next;
113};
114Q_DECLARE_TYPEINFO(MetaStackEntry, Q_RELOCATABLE_TYPE);
115
116/*!
117 Start accumulating values in a list by appending an empty
118 string to the list.
119 */
120void MetaStackEntry::open()
121{
122 next.append(t: QString());
123}
124
125/*!
126 Stop accumulating values and append the list of accumulated
127 values to the complete list of accumulated values.
128
129 */
130void MetaStackEntry::close()
131{
132 accum += next;
133 next.clear();
134}
135
136/*!
137 \class MetaStack
138
139 This class maintains a stack of values of config file variables.
140*/
141class MetaStack : private QStack<MetaStackEntry>
142{
143public:
144 MetaStack();
145
146 void process(QChar ch, const Location &location);
147 QStringList getExpanded(const Location &location);
148};
149
150/*!
151 The default constructor pushes a new stack entry and
152 opens it.
153 */
154MetaStack::MetaStack()
155{
156 push(t: MetaStackEntry());
157 top().open();
158}
159
160/*!
161 Processes the character \a ch using the \a location.
162 It really just builds up a name by appending \a ch to
163 it.
164 */
165void MetaStack::process(QChar ch, const Location &location)
166{
167 if (ch == QLatin1Char('{')) {
168 push(t: MetaStackEntry());
169 top().open();
170 } else if (ch == QLatin1Char('}')) {
171 if (size() == 1)
172 location.fatal(QStringLiteral("Unexpected '}'"));
173
174 top().close();
175 const QStringList suffixes = pop().accum;
176 const QStringList prefixes = top().next;
177
178 top().next.clear();
179 for (const auto &prefix : prefixes) {
180 for (const auto &suffix : suffixes)
181 top().next << prefix + suffix;
182 }
183 } else if (ch == QLatin1Char(',') && size() > 1) {
184 top().close();
185 top().open();
186 } else {
187 for (QString &topNext : top().next)
188 topNext += ch;
189 }
190}
191
192/*!
193 Returns the accumulated string values.
194 */
195QStringList MetaStack::getExpanded(const Location &location)
196{
197 if (size() > 1)
198 location.fatal(QStringLiteral("Missing '}'"));
199
200 top().close();
201 return top().accum;
202}
203
204const QString Config::dot = QLatin1String(".");
205bool Config::m_debug = false;
206bool Config::m_atomsDump = false;
207bool Config::generateExamples = true;
208QString Config::overrideOutputDir;
209QString Config::installDir;
210QSet<QString> Config::overrideOutputFormats;
211QMap<QString, QString> Config::m_extractedDirs;
212QStack<QString> Config::m_workingDirs;
213QMap<QString, QStringList> Config::m_includeFilesMap;
214
215/*!
216 \class ConfigVar
217 \brief contains all the information for a single config variable in a
218 .qdocconf file.
219*/
220
221/*!
222 Returns this configuration variable as a string.
223
224 If the variable is not defined, returns \a defaultString.
225
226 \note By default, \a defaultString is a null string.
227 This allows determining whether a configuration variable is
228 undefined (returns a null string) or defined as empty
229 (returns a non-null, empty string).
230*/
231QString ConfigVar::asString(const QString defaultString) const
232{
233 if (m_name.isEmpty())
234 return defaultString;
235
236 QString result(""); // an empty but non-null string
237 for (const auto &value : std::as_const(t: m_values)) {
238 if (!result.isEmpty() && !result.endsWith(c: QChar('\n')))
239 result.append(c: QChar(' '));
240 result.append(s: value.m_value);
241 }
242 return result;
243}
244
245/*!
246 Returns this config variable as a string list.
247*/
248QStringList ConfigVar::asStringList() const
249{
250 QStringList result;
251 for (const auto &value : std::as_const(t: m_values))
252 result << value.m_value;
253 return result;
254}
255
256/*!
257 Returns this config variable as a string set.
258*/
259QSet<QString> ConfigVar::asStringSet() const
260{
261 const auto &stringList = asStringList();
262 return QSet<QString>(stringList.cbegin(), stringList.cend());
263}
264
265/*!
266 Returns this config variable as a boolean.
267*/
268bool ConfigVar::asBool() const
269{
270 return QVariant(asString()).toBool();
271}
272
273/*!
274 Returns this configuration variable as an integer; iterates
275 through the string list, interpreting each
276 string in the list as an integer and adding it to a total sum.
277
278 Returns 0 if this variable is defined as empty, and
279 -1 if it's is not defined.
280 */
281int ConfigVar::asInt() const
282{
283 const QStringList strs = asStringList();
284 if (strs.isEmpty())
285 return -1;
286
287 int sum = 0;
288 for (const auto &str : strs)
289 sum += str.toInt();
290 return sum;
291}
292
293/*!
294 Appends values to this ConfigVar, and adjusts the ExpandVar
295 parameters so that they continue to refer to the correct values.
296*/
297void ConfigVar::append(const ConfigVar &other)
298{
299 m_expandVars << other.m_expandVars;
300 QList<ExpandVar>::Iterator it = m_expandVars.end();
301 it -= other.m_expandVars.size();
302 std::for_each(first: it, last: m_expandVars.end(), f: [this](ExpandVar &v) {
303 v.m_valueIndex += m_values.size();
304 });
305 m_values << other.m_values;
306 m_location = other.m_location;
307}
308
309/*!
310 \class Config
311 \brief The Config class contains the configuration variables
312 for controlling how qdoc produces documentation.
313
314 Its load() function reads, parses, and processes a qdocconf file.
315 */
316
317/*!
318 \enum Config::PathFlags
319
320 Flags used for retrieving canonicalized paths from Config.
321
322 \value Validate
323 Issue a warning for paths that do not exist and
324 remove them from the returned list.
325
326 \value IncludePaths
327 Assume the variable contains include paths with
328 prefixes such as \c{-I} that are to be removed
329 before canonicalizing and then re-inserted.
330
331 \omitvalue None
332
333 \sa getCanonicalPathList()
334*/
335
336/*!
337 Initializes the Config with \a programName and sets all
338 internal state variables to either default values or to ones
339 defined in command line arguments \a args.
340 */
341void Config::init(const QString &programName, const QStringList &args)
342{
343 m_prog = programName;
344 processCommandLineOptions(args);
345 reset();
346}
347
348Config::~Config()
349{
350 clear();
351}
352
353/*!
354 Clears the location and internal maps for config variables.
355 */
356void Config::clear()
357{
358 m_location = Location();
359 m_configVars.clear();
360 m_includeFilesMap.clear();
361 m_excludedPaths.reset();
362 m_sourceLink.reset();
363}
364
365/*!
366 Resets the Config instance - used by load()
367 */
368void Config::reset()
369{
370 clear();
371
372 // Default values
373 setStringList(CONFIG_CODEINDENT, values: QStringList("0"));
374 setStringList(CONFIG_FALSEHOODS, values: QStringList("0"));
375 setStringList(CONFIG_HEADERS + dot + CONFIG_FILEEXTENSIONS, values: QStringList("*.ch *.h *.h++ *.hh *.hpp *.hxx"));
376 setStringList(CONFIG_SOURCES + dot + CONFIG_FILEEXTENSIONS, values: QStringList("*.c++ *.cc *.cpp *.cxx *.mm *.qml *.qdoc"));
377 setStringList(CONFIG_LANGUAGE, values: QStringList("Cpp")); // i.e. C++
378 setStringList(CONFIG_OUTPUTFORMATS, values: QStringList("HTML"));
379 setStringList(CONFIG_TABSIZE, values: QStringList("8"));
380 setStringList(CONFIG_LOCATIONINFO, values: QStringList("true"));
381 setStringList(CONFIG_WARNABOUTMISSINGIMAGES, values: QStringList("true"));
382 setStringList(CONFIG_WARNABOUTMISSINGPROJECTFILES, values: QStringList("true"));
383
384 // Publish options from the command line as config variables
385 const auto setListFlag = [this](const QString &key, bool test) {
386 setStringList(var: key, values: QStringList(test ? QStringLiteral("true") : QStringLiteral("false")));
387 };
388#define SET(opt, test) setListFlag(opt, m_parser.isSet(m_parser.test))
389 SET(CONFIG_SYNTAXHIGHLIGHTING, highlightingOption);
390 SET(CONFIG_SHOWINTERNAL, showInternalOption);
391 SET(CONFIG_SINGLEEXEC, singleExecOption);
392 SET(CONFIG_REDIRECTDOCUMENTATIONTODEVNULL, redirectDocumentationToDevNullOption);
393 SET(CONFIG_AUTOLINKERRORS, autoLinkErrorsOption);
394#undef SET
395 m_showInternal = m_configVars.value(CONFIG_SHOWINTERNAL).asBool();
396 setListFlag(CONFIG_NOLINKERRORS,
397 m_parser.isSet(option: m_parser.noLinkErrorsOption)
398 || qEnvironmentVariableIsSet(varName: "QDOC_NOLINKERRORS"));
399
400 // CONFIG_DEFINES and CONFIG_INCLUDEPATHS are set in load()
401}
402
403/*!
404 Loads and parses the qdoc configuration file \a fileName.
405 If a previous project was loaded, this function first resets the
406 Config instance. Then it calls the other load() function, which
407 does the loading, parsing, and processing of the configuration file.
408 */
409void Config::load(const QString &fileName)
410{
411 // Reset if a previous project was loaded
412 if (m_configVars.contains(CONFIG_PROJECT))
413 reset();
414
415 load(location: Location(), fileName);
416 if (m_location.isEmpty())
417 m_location = Location(fileName);
418 else
419 m_location.setEtc(true);
420
421 // Resolve variables that are interpreted as paths
422 QString varName{CONFIG_URL + dot + CONFIG_SOURCES + dot + CONFIG_ROOTDIR};
423 setStringList(var: varName, values: getCanonicalPathList(var: varName, flags: Validate));
424
425 expandVariables();
426
427 // Add defines and includepaths from command line to their
428 // respective configuration variables. Values set here are
429 // always added to what's defined in configuration file.
430 insertStringList(CONFIG_DEFINES, values: m_defines);
431 insertStringList(CONFIG_INCLUDEPATHS, values: m_includePaths);
432
433 // Prefetch values that are used internally
434 m_exampleFiles = getCanonicalPathList(CONFIG_EXAMPLES);
435 m_exampleDirs = getCanonicalPathList(CONFIG_EXAMPLEDIRS);
436 m_reportMissingAltTextForImages =
437 m_configVars.value(CONFIG_REPORTMISSINGALTTEXTFORIMAGES).asBool();
438}
439
440/*!
441 Expands other config variables referred to in all stored ConfigVars.
442*/
443void Config::expandVariables()
444{
445 for (auto &configVar : m_configVars) {
446 for (auto it = configVar.m_expandVars.crbegin(); it != configVar.m_expandVars.crend(); ++it) {
447 Q_ASSERT(it->m_valueIndex < configVar.m_values.size());
448 const QString &key = it->m_var;
449 const auto &refVar = m_configVars.value(key);
450 if (refVar.m_name.isEmpty()) {
451 configVar.m_location.fatal(
452 QStringLiteral("Environment or configuration variable '%1' undefined")
453 .arg(a: it->m_var));
454 } else if (!refVar.m_expandVars.empty()) {
455 configVar.m_location.fatal(
456 QStringLiteral("Nested variable expansion not allowed"),
457 QStringLiteral("When expanding '%1' at %2:%3")
458 .arg(args: refVar.m_name, args: refVar.m_location.filePath(),
459 args: QString::number(refVar.m_location.lineNo())));
460 }
461 QString expanded;
462 if (it->m_delim.isNull())
463 expanded = m_configVars.value(key).asStringList().join(sep: QString());
464 else
465 expanded = m_configVars.value(key).asStringList().join(sep: it->m_delim);
466 configVar.m_values[it->m_valueIndex].m_value.insert(i: it->m_index, s: expanded);
467 }
468 configVar.m_expandVars.clear();
469 }
470}
471
472/*!
473 Sets the \a values of a configuration variable \a var from a string list.
474 */
475void Config::setStringList(const QString &var, const QStringList &values)
476{
477 m_configVars.insert(key: var, value: ConfigVar(var, values, QDir::currentPath()));
478}
479
480/*!
481 Adds the \a values from a string list to the configuration variable \a var.
482 Existing value(s) are kept.
483*/
484void Config::insertStringList(const QString &var, const QStringList &values)
485{
486 m_configVars[var].append(other: ConfigVar(var, values, QDir::currentPath()));
487}
488
489/*!
490 Process and store variables from the command line.
491 */
492void Config::processCommandLineOptions(const QStringList &args)
493{
494 m_parser.process(arguments: args);
495
496 m_defines = m_parser.values(option: m_parser.defineOption);
497 m_dependModules = m_parser.values(option: m_parser.dependsOption);
498 setIndexDirs();
499 setIncludePaths();
500
501 generateExamples = !m_parser.isSet(option: m_parser.noExamplesOption);
502 if (m_parser.isSet(option: m_parser.installDirOption))
503 installDir = m_parser.value(option: m_parser.installDirOption);
504 if (m_parser.isSet(option: m_parser.outputDirOption))
505 overrideOutputDir = QDir(m_parser.value(option: m_parser.outputDirOption)).absolutePath();
506
507 const auto outputFormats = m_parser.values(option: m_parser.outputFormatOption);
508 for (const auto &format : outputFormats)
509 overrideOutputFormats.insert(value: format);
510 m_debug = m_parser.isSet(option: m_parser.debugOption) || qEnvironmentVariableIsSet(varName: "QDOC_DEBUG");
511 m_atomsDump = m_parser.isSet(option: m_parser.atomsDumpOption);
512 m_showInternal = m_parser.isSet(option: m_parser.showInternalOption)
513 || qEnvironmentVariableIsSet(varName: "QDOC_SHOW_INTERNAL");
514
515 if (m_parser.isSet(option: m_parser.prepareOption))
516 m_qdocPass = Prepare;
517 if (m_parser.isSet(option: m_parser.generateOption))
518 m_qdocPass = Generate;
519 if (m_debug || m_parser.isSet(option: m_parser.logProgressOption))
520 setStringList(CONFIG_LOGPROGRESS, values: QStringList("true"));
521 if (m_parser.isSet(option: m_parser.timestampsOption))
522 setStringList(CONFIG_TIMESTAMPS, values: QStringList("true"));
523 if (m_parser.isSet(option: m_parser.useDocBookExtensions))
524 setStringList(CONFIG_DOCBOOKEXTENSIONS, values: QStringList("true"));
525}
526
527void Config::setIncludePaths()
528{
529 QDir currentDir = QDir::current();
530 const auto addIncludePaths = [this, currentDir](const char *flag, const QStringList &paths) {
531 for (const auto &path : paths)
532 m_includePaths << currentDir.absoluteFilePath(fileName: path).insert(i: 0, s: flag);
533 };
534
535 addIncludePaths("-I", m_parser.values(option: m_parser.includePathOption));
536#ifdef QDOC_PASS_ISYSTEM
537 addIncludePaths("-isystem", m_parser.values(m_parser.includePathSystemOption));
538#endif
539 addIncludePaths("-F", m_parser.values(option: m_parser.frameworkOption));
540}
541
542/*!
543 Stores paths from -indexdir command line option(s).
544 */
545void Config::setIndexDirs()
546{
547 m_indexDirs = m_parser.values(option: m_parser.indexDirOption);
548 auto it = std::remove_if(first: m_indexDirs.begin(), last: m_indexDirs.end(),
549 pred: [](const QString &s) { return !QFile::exists(fileName: s); });
550
551 std::for_each(first: it, last: m_indexDirs.end(), f: [](const QString &s) {
552 qCWarning(lcQdoc) << "Cannot find index directory: " << s;
553 });
554 m_indexDirs.erase(abegin: it, aend: m_indexDirs.end());
555}
556
557/*!
558 Function to return the correct outputdir for the output \a format.
559 If \a format is not specified, defaults to 'HTML'.
560 outputdir can be set using the qdocconf or the command-line
561 variable -outputdir.
562 */
563QString Config::getOutputDir(const QString &format) const
564{
565 QString t;
566 if (overrideOutputDir.isNull())
567 t = m_configVars.value(CONFIG_OUTPUTDIR).asString();
568 else
569 t = overrideOutputDir;
570 if (m_configVars.value(CONFIG_SINGLEEXEC).asBool()) {
571 QString project = m_configVars.value(CONFIG_PROJECT).asString();
572 t += QLatin1Char('/') + project.toLower();
573 }
574 if (m_configVars.value(key: format + Config::dot + "nosubdirs").asBool()) {
575 QString singleOutputSubdir = m_configVars.value(key: format + Config::dot + "outputsubdir").asString();
576 if (singleOutputSubdir.isEmpty())
577 singleOutputSubdir = "html";
578 t += QLatin1Char('/') + singleOutputSubdir;
579 }
580 return QDir::cleanPath(path: t);
581}
582
583/*!
584 Function to return the correct outputformats.
585 outputformats can be set using the qdocconf or the command-line
586 variable -outputformat.
587 */
588QSet<QString> Config::getOutputFormats() const
589{
590 if (overrideOutputFormats.isEmpty())
591 return m_configVars.value(CONFIG_OUTPUTFORMATS).asStringSet();
592 else
593 return overrideOutputFormats;
594}
595
596// TODO: [late-canonicalization][pod-configuration]
597// The canonicalization for paths is done at the time where they are
598// required, and done each time they are requested.
599// Instead, config should be parsed to an intermediate format that is
600// a POD type that already contains canonicalized representations for
601// each element.
602// Those representations should provide specific guarantees about
603// their format and be representable at the API boundaries.
604//
605// This would ensure that the correct canonicalization is always
606// applied, is applied only once and that dependent sub-logics can be
607// written in a way that doesn't require branching or futher
608// canonicalization.
609
610/*!
611 Returns a path list where all paths from the config variable \a var
612 are canonicalized. If \a flags contains \c Validate, outputs a warning
613 for invalid paths. The \c IncludePaths flag is used as a hint to strip
614 away potential prefixes found in include paths before attempting to
615 canonicalize.
616 */
617QStringList Config::getCanonicalPathList(const QString &var, PathFlags flags) const
618{
619 QStringList result;
620 const auto &configVar = m_configVars.value(key: var);
621
622 for (const auto &value : configVar.m_values) {
623 const QString &currentPath = value.m_path;
624 QString rawValue = value.m_value.simplified();
625 QString prefix;
626
627 if (flags & IncludePaths) {
628 const QStringList prefixes = QStringList()
629 << QLatin1String("-I")
630 << QLatin1String("-F")
631 << QLatin1String("-isystem");
632 const auto end = std::end(cont: prefixes);
633 const auto it =
634 std::find_if(first: std::begin(cont: prefixes), last: end,
635 pred: [&rawValue](const QString &p) {
636 return rawValue.startsWith(s: p);
637 });
638 if (it != end) {
639 prefix = *it;
640 rawValue.remove(i: 0, len: it->size());
641 if (rawValue.isEmpty())
642 continue;
643 } else {
644 prefix = prefixes[0]; // -I as default
645 }
646 }
647
648 QDir dir(rawValue.trimmed());
649 const QString path = dir.path();
650
651 if (dir.isRelative())
652 dir.setPath(currentPath + QLatin1Char('/') + path);
653 if ((flags & Validate) && !QFileInfo::exists(file: dir.path()))
654 configVar.m_location.warning(QStringLiteral("Cannot find file or directory: %1").arg(a: path));
655 else {
656 const QString canonicalPath = dir.canonicalPath();
657 if (!canonicalPath.isEmpty())
658 result.append(t: prefix + canonicalPath);
659 else if (path.contains(c: QLatin1Char('*')) || path.contains(c: QLatin1Char('?')))
660 result.append(t: path);
661 else
662 qCDebug(lcQdoc) <<
663 qUtf8Printable(QStringLiteral("%1: Ignored nonexistent path \'%2\'")
664 .arg(configVar.m_location.toString(), rawValue));
665 }
666 }
667 return result;
668}
669
670/*!
671 Calls getRegExpList() with the control variable \a var and
672 iterates through the resulting list of regular expressions,
673 concatenating them with extra characters to form a single
674 QRegularExpression, which is then returned.
675
676 \sa getRegExpList()
677 */
678QRegularExpression Config::getRegExp(const QString &var) const
679{
680 QString pattern;
681 const auto subRegExps = getRegExpList(var);
682
683 for (const auto &regExp : subRegExps) {
684 if (!regExp.isValid())
685 return regExp;
686 if (!pattern.isEmpty())
687 pattern += QLatin1Char('|');
688 pattern += QLatin1String("(?:") + regExp.pattern() + QLatin1Char(')');
689 }
690 if (pattern.isEmpty())
691 pattern = QLatin1String("$x"); // cannot match
692 return QRegularExpression(pattern);
693}
694
695/*!
696 Looks up the configuration variable \a var in the string list
697 map, converts the string list to a list of regular expressions,
698 and returns it.
699 */
700QList<QRegularExpression> Config::getRegExpList(const QString &var) const
701{
702 const QStringList strs = m_configVars.value(key: var).asStringList();
703 QList<QRegularExpression> regExps;
704 for (const auto &str : strs)
705 regExps += QRegularExpression(str);
706 return regExps;
707}
708
709/*!
710 This function is slower than it could be. What it does is
711 find all the keys that begin with \a var + dot and return
712 the matching keys in a set, stripped of the matching prefix
713 and dot.
714 */
715QSet<QString> Config::subVars(const QString &var) const
716{
717 QSet<QString> result;
718 QString varDot = var + QLatin1Char('.');
719 for (auto it = m_configVars.constBegin(); it != m_configVars.constEnd(); ++it) {
720 if (it.key().startsWith(s: varDot)) {
721 QString subVar = it.key().mid(position: varDot.size());
722 int dot = subVar.indexOf(ch: QLatin1Char('.'));
723 if (dot != -1)
724 subVar.truncate(pos: dot);
725 result.insert(value: subVar);
726 }
727 }
728 return result;
729}
730
731/*!
732 Searches for a path to \a fileName in 'sources', 'sourcedirs', and
733 'exampledirs' config variables and returns a full path to the first
734 match found. If the file is not found, returns an empty string.
735 */
736QString Config::getIncludeFilePath(const QString &fileName) const
737{
738 QString ext = QFileInfo(fileName).suffix();
739
740 if (!m_includeFilesMap.contains(key: ext)) {
741 QStringList result = getCanonicalPathList(CONFIG_SOURCES);
742 result.erase(abegin: std::remove_if(first: result.begin(), last: result.end(),
743 pred: [&](const QString &s) { return !s.endsWith(s: ext); }),
744 aend: result.end());
745 const QStringList dirs =
746 getCanonicalPathList(CONFIG_SOURCEDIRS) +
747 getCanonicalPathList(CONFIG_EXAMPLEDIRS);
748
749 for (const auto &dir : dirs)
750 result += getFilesHere(dir, nameFilter: "*." + ext, location: location());
751 result.removeDuplicates();
752 m_includeFilesMap.insert(key: ext, value: result);
753 }
754 const QStringList &paths = (*m_includeFilesMap.find(key: ext));
755 QString match = fileName;
756 if (!match.startsWith(c: '/'))
757 match.prepend(c: '/');
758 for (const auto &path : paths) {
759 if (path.endsWith(s: match))
760 return path;
761 }
762 return QString();
763}
764
765/*!
766 Builds and returns a list of file pathnames for the file
767 type specified by \a filesVar (e.g. "headers" or "sources").
768 The files are found in the directories specified by
769 \a dirsVar, and they are filtered by \a defaultNameFilter
770 if a better filter can't be constructed from \a filesVar.
771 The directories in \a excludedDirs are avoided. The files
772 in \a excludedFiles are not included in the return list.
773 */
774QStringList Config::getAllFiles(const QString &filesVar, const QString &dirsVar,
775 const QSet<QString> &excludedDirs,
776 const QSet<QString> &excludedFiles)
777{
778 QStringList result = getCanonicalPathList(var: filesVar, flags: Validate);
779 const QStringList dirs = getCanonicalPathList(var: dirsVar, flags: Validate);
780
781 const QString nameFilter = m_configVars.value(key: filesVar + dot + CONFIG_FILEEXTENSIONS).asString();
782
783 for (const auto &dir : dirs)
784 result += getFilesHere(dir, nameFilter, location: location(), excludedDirs, excludedFiles);
785 return result;
786}
787
788QStringList Config::getExampleQdocFiles(const QSet<QString> &excludedDirs,
789 const QSet<QString> &excludedFiles)
790{
791 QStringList result;
792 const QStringList dirs = getCanonicalPathList(var: "exampledirs");
793 const QString nameFilter = " *.qdoc";
794
795 for (const auto &dir : dirs)
796 result += getFilesHere(dir, nameFilter, location: location(), excludedDirs, excludedFiles);
797 return result;
798}
799
800QStringList Config::getExampleImageFiles(const QSet<QString> &excludedDirs,
801 const QSet<QString> &excludedFiles)
802{
803 QStringList result;
804 const QStringList dirs = getCanonicalPathList(var: "exampledirs");
805 const QString nameFilter = m_configVars.value(CONFIG_EXAMPLES + dot + CONFIG_IMAGEEXTENSIONS).asString();
806
807 for (const auto &dir : dirs)
808 result += getFilesHere(dir, nameFilter, location: location(), excludedDirs, excludedFiles);
809 return result;
810}
811
812// TODO: [misplaced-logic][examples][pod-configuration]
813// The definition of how an example is structured and how to find its
814// components should not be part of Config or, for that matter,
815// CppCodeParser, which is the actual caller of this method.
816// Move this method to a more appropriate place as soon as a suitable
817// place is available for it.
818
819/*!
820 Returns the path to the project file for \a examplePath, or an empty string
821 if no project file was found.
822 */
823QString Config::getExampleProjectFile(const QString &examplePath)
824{
825 QFileInfo fileInfo(examplePath);
826 QStringList validNames;
827 validNames << QLatin1String("CMakeLists.txt")
828 << fileInfo.fileName() + QLatin1String(".pro")
829 << fileInfo.fileName() + QLatin1String(".qmlproject")
830 << fileInfo.fileName() + QLatin1String(".pyproject")
831 << QLatin1String("qbuild.pro"); // legacy
832
833 QString projectFile;
834
835 for (const auto &name : std::as_const(t&: validNames)) {
836 projectFile = Config::findFile(location: Location(), files: m_exampleFiles, dirs: m_exampleDirs,
837 fileName: examplePath + QLatin1Char('/') + name);
838 if (!projectFile.isEmpty())
839 return projectFile;
840 }
841
842 return projectFile;
843}
844
845// TODO: [pod-configuration]
846// Remove findFile completely from the configuration.
847// External usages of findFile were already removed but a last caller
848// of this method exists internally to Config in
849// `getExampleProjectFile`.
850// That method has to be removed at some point and this method should
851// go with it.
852// Do notice that FileResolver is the replacement for findFile but it
853// is designed, for now, with a scope that does only care about the
854// usages of findFile that are outside the Config class.
855// More specifically, it was designed to replace only the uses of
856// findFile that deal with user provided queries or queries related to
857// that.
858// The logic that is used internally in Config is the same, but has a
859// different conceptual meaning.
860// When findFile is permanently removed, it must be considered whether
861// FileResolver itself should be used for the same logic or not.
862
863/*!
864 \a fileName is the path of the file to find.
865
866 \a files and \a dirs are the lists where we must find the
867 components of \a fileName.
868
869 \a location is used for obtaining the file and line numbers
870 for report qdoc errors.
871 */
872QString Config::findFile(const Location &location, const QStringList &files,
873 const QStringList &dirs, const QString &fileName,
874 QString *userFriendlyFilePath)
875{
876 if (fileName.isEmpty() || fileName.startsWith(c: QLatin1Char('/'))) {
877 if (userFriendlyFilePath)
878 *userFriendlyFilePath = fileName;
879 return fileName;
880 }
881
882 QFileInfo fileInfo;
883 QStringList components = fileName.split(sep: QLatin1Char('?'));
884 QString firstComponent = components.first();
885
886 for (const auto &file : files) {
887 if (file == firstComponent || file.endsWith(s: QLatin1Char('/') + firstComponent)) {
888 fileInfo.setFile(file);
889 if (!fileInfo.exists())
890 location.fatal(QStringLiteral("File '%1' does not exist").arg(a: file));
891 break;
892 }
893 }
894
895 if (fileInfo.fileName().isEmpty()) {
896 for (const auto &dir : dirs) {
897 fileInfo.setFile(dir: QDir(dir), file: firstComponent);
898 if (fileInfo.exists())
899 break;
900 }
901 }
902
903 if (userFriendlyFilePath)
904 userFriendlyFilePath->clear();
905 if (!fileInfo.exists())
906 return QString();
907
908 // <<REMARK: This is actually dead code. It is unclear what it tries
909 // to do and why but its usage is unnecessary in the current
910 // codebase.
911 // Indeed, the whole concept of the "userFriendlyFilePath" is
912 // removed for file searching.
913 // It will be removed directly with the whole of findFile, but it
914 // should not be considered anymore until then.
915 if (userFriendlyFilePath) {
916 for (auto c = components.constBegin();;) {
917 bool isArchive = (c != components.constEnd() - 1);
918 userFriendlyFilePath->append(s: *c);
919
920 if (isArchive) {
921 QString extracted = m_extractedDirs[fileInfo.filePath()];
922
923 ++c;
924 fileInfo.setFile(dir: QDir(extracted), file: *c);
925 } else {
926 break;
927 }
928
929 userFriendlyFilePath->append(c: QLatin1Char('?'));
930 }
931 }
932 // REMARK>>
933
934 return fileInfo.filePath();
935}
936
937// TODO: [pod-configuration]
938// An intermediate representation for the configuration should only
939// contain data that will later be destructured into subsystem that
940// care about specific subsets of the configuration and can carry that
941// information with them, uniquely.
942// Remove copyFile, moving it into whatever will have the unique
943// resposability of knowing how to build an output directory for a
944// QDoc execution.
945// Should copy file being used for not only copying file to the build
946// output directory, split its responsabilities into smaller elements
947// instead of forcing the logic together.
948
949/*!
950 Copies the \a sourceFilePath to the file name constructed by
951 concatenating \a targetDirPath and the file name from the
952 \a userFriendlySourceFilePath. \a location is for identifying
953 the file and line number where a qdoc error occurred. The
954 constructed output file name is returned.
955 */
956QString Config::copyFile(const Location &location, const QString &sourceFilePath,
957 const QString &userFriendlySourceFilePath, const QString &targetDirPath)
958{
959 // TODO: A copying operation should only be performed on files
960 // that we assume to be available. Ensure that this is true at the
961 // API boundary and bubble up the error checking and reporting to
962 // call-site users. Possibly this will be as simple as
963 // ResolvedFile, but could not be done at the time of the introduction of
964 // that type as we first need to encapsulate the logic for
965 // copying files into an appropriate subsystem and have a better
966 // understanding of call-site usages.
967
968 QFile inFile(sourceFilePath);
969 if (!inFile.open(flags: QFile::ReadOnly)) {
970 location.warning(QStringLiteral("Cannot open input file for copy: '%1': %2")
971 .arg(args: sourceFilePath, args: inFile.errorString()));
972 return QString();
973 }
974
975 // TODO: [non-canonical-representation]
976 // Similar to other part of QDoc, we do a series of non-intuitive
977 // checks to canonicalize some multi-format parameter into
978 // something we can use.
979 // Understand which of those formats are actually in use and
980 // provide a canonicalized version that can be requested at the
981 // API boundary to ensure that correct formatting is used.
982 // If possible, gradually bubble up the canonicalization until a
983 // single entry-point in the program exists where the
984 // canonicalization can be processed to avoid complicating
985 // intermediate steps.
986 // ADDENDUM 1: At least one usage of this seems to depend on the
987 // processing done for files coming from
988 // Generator::copyTemplateFile, which are expressed as absolute
989 // paths. This seems to be the only usage that is currently
990 // needed, hence a temporary new implementation is provided that
991 // only takes this case into account.
992 // Do notice that we assume that in this case we always want a
993 // flat structure, that is, we are copying the file as a direct
994 // child of the target directory.
995 // Nonetheless, it is possible that this case will not be needed,
996 // such that it can be removed later on, or that it will be nedeed
997 // in multiple places such that an higher level interface for it
998 // should be provided.
999 // Furthermoe, it might be possible that there is an edge case
1000 // that is now not considered, as it is unknown, that was
1001 // considered before.
1002 // As it is now unclear what kind of paths are used here, what
1003 // format they have, why they are used and why they have some
1004 // specific format, further processing is avoided but a more
1005 // torough overview of what should is needed must be done when
1006 // more information are gathered and this function is extracted
1007 // away from config.
1008
1009 QString outFileName{userFriendlySourceFilePath};
1010 QFileInfo outFileNameInfo{userFriendlySourceFilePath};
1011 if (outFileNameInfo.isAbsolute())
1012 outFileName = outFileNameInfo.fileName();
1013
1014 outFileName = targetDirPath + "/" + outFileName;
1015 QDir targetDir(targetDirPath);
1016 if (!targetDir.exists())
1017 targetDir.mkpath(dirPath: ".");
1018
1019 QFile outFile(outFileName);
1020 if (!outFile.open(flags: QFile::WriteOnly)) {
1021 // TODO: [uncrentralized-warning]
1022 location.warning(QStringLiteral("Cannot open output file for copy: '%1': %2")
1023 .arg(args&: outFileName, args: outFile.errorString()));
1024 return QString();
1025 }
1026
1027 // TODO: There shouldn't be any particular advantage to copying
1028 // the file by readying its content and writing it compared to
1029 // asking the underlying system to do the copy for us.
1030 // Consider simplifying this part by avoiding doing the manual
1031 // work ourselves.
1032
1033 char buffer[1024];
1034 qsizetype len;
1035 while ((len = inFile.read(data: buffer, maxlen: sizeof(buffer))) > 0)
1036 outFile.write(data: buffer, len);
1037 return outFileName;
1038}
1039
1040/*!
1041 Finds the largest unicode digit in \a value in the range
1042 1..7 and returns it.
1043 */
1044int Config::numParams(const QString &value)
1045{
1046 int max = 0;
1047 for (int i = 0; i != value.size(); ++i) {
1048 uint c = value[i].unicode();
1049 if (c > 0 && c < 8)
1050 max = qMax(a: max, b: static_cast<int>(c));
1051 }
1052 return max;
1053}
1054
1055/*!
1056 Returns \c true if \a ch is a letter, number, '_', '.',
1057 '{', '}', or ','.
1058 */
1059bool Config::isMetaKeyChar(QChar ch)
1060{
1061 return ch.isLetterOrNumber() || ch == QLatin1Char('_') || ch == QLatin1Char('.')
1062 || ch == QLatin1Char('{') || ch == QLatin1Char('}') || ch == QLatin1Char(',');
1063}
1064
1065/*!
1066 \a fileName is a master qdocconf file. It contains a list of
1067 qdocconf files and nothing else. Read the list and return it.
1068 */
1069QStringList Config::loadMaster(const QString &fileName)
1070{
1071 Location location;
1072 QFile fin(fileName);
1073 if (!fin.open(flags: QFile::ReadOnly | QFile::Text)) {
1074 if (!Config::installDir.isEmpty()) {
1075 qsizetype prefix = location.filePath().size() - location.fileName().size();
1076 fin.setFileName(Config::installDir + QLatin1Char('/')
1077 + fileName.right(n: fileName.size() - prefix));
1078 }
1079 if (!fin.open(flags: QFile::ReadOnly | QFile::Text))
1080 location.fatal(QStringLiteral("Cannot open master qdocconf file '%1': %2")
1081 .arg(args: fileName, args: fin.errorString()));
1082 }
1083 QTextStream stream(&fin);
1084 QStringList qdocFiles;
1085 QDir configDir(QFileInfo(fileName).canonicalPath());
1086 QString line = stream.readLine();
1087 while (!line.isNull()) {
1088 if (!line.isEmpty())
1089 qdocFiles.append(t: QFileInfo(configDir, line).filePath());
1090 line = stream.readLine();
1091 }
1092 fin.close();
1093 return qdocFiles;
1094}
1095
1096/*!
1097 Load, parse, and process a qdoc configuration file. This
1098 function is only called by the other load() function, but
1099 this one is recursive, i.e., it calls itself when it sees
1100 an \c{include} statement in the qdoc configuration file.
1101 */
1102void Config::load(Location location, const QString &fileName)
1103{
1104 QFileInfo fileInfo(fileName);
1105 pushWorkingDir(dir: fileInfo.canonicalPath());
1106 static const QRegularExpression keySyntax(QRegularExpression::anchoredPattern(expression: QLatin1String("\\w+(?:\\.\\w+)*")));
1107
1108#define SKIP_CHAR() \
1109 do { \
1110 location.advance(c); \
1111 ++i; \
1112 c = text.at(i); \
1113 cc = c.unicode(); \
1114 } while (0)
1115
1116#define SKIP_SPACES() \
1117 while (c.isSpace() && cc != '\n') \
1118 SKIP_CHAR()
1119
1120#define PUT_CHAR() \
1121 word += c; \
1122 SKIP_CHAR();
1123
1124 if (location.depth() > 16)
1125 location.fatal(QStringLiteral("Too many nested includes"));
1126
1127 QFile fin(fileInfo.fileName());
1128 if (!fin.open(flags: QFile::ReadOnly | QFile::Text)) {
1129 if (!Config::installDir.isEmpty()) {
1130 qsizetype prefix = location.filePath().size() - location.fileName().size();
1131 fin.setFileName(Config::installDir + QLatin1Char('/')
1132 + fileName.right(n: fileName.size() - prefix));
1133 }
1134 if (!fin.open(flags: QFile::ReadOnly | QFile::Text))
1135 location.fatal(
1136 QStringLiteral("Cannot open file '%1': %2").arg(args: fileName, args: fin.errorString()));
1137 }
1138
1139 QTextStream stream(&fin);
1140 QString text = stream.readAll();
1141 text += QLatin1String("\n\n");
1142 text += QLatin1Char('\0');
1143 fin.close();
1144
1145 location.push(filePath: fileName);
1146 location.start();
1147
1148 int i = 0;
1149 QChar c = text.at(i: 0);
1150 uint cc = c.unicode();
1151 while (i < text.size()) {
1152 if (cc == 0) {
1153 ++i;
1154 } else if (c.isSpace()) {
1155 SKIP_CHAR();
1156 } else if (cc == '#') {
1157 do {
1158 SKIP_CHAR();
1159 } while (cc != '\n');
1160 } else if (isMetaKeyChar(ch: c)) {
1161 Location keyLoc = location;
1162 bool plus = false;
1163 QStringList rhsValues;
1164 QList<ExpandVar> expandVars;
1165 QString word;
1166 bool inQuote = false;
1167 bool needsExpansion = false;
1168
1169 MetaStack stack;
1170 do {
1171 stack.process(ch: c, location);
1172 SKIP_CHAR();
1173 } while (isMetaKeyChar(ch: c));
1174
1175 const QStringList keys = stack.getExpanded(location);
1176 SKIP_SPACES();
1177
1178 if (keys.size() == 1 && keys.first() == QLatin1String("include")) {
1179 QString includeFile;
1180
1181 if (cc != '(')
1182 location.fatal(QStringLiteral("Bad include syntax"));
1183 SKIP_CHAR();
1184 SKIP_SPACES();
1185
1186 while (!c.isSpace() && cc != '#' && cc != ')') {
1187
1188 if (cc == '$') {
1189 QString var;
1190 SKIP_CHAR();
1191 while (c.isLetterOrNumber() || cc == '_') {
1192 var += c;
1193 SKIP_CHAR();
1194 }
1195 if (!var.isEmpty()) {
1196 const QByteArray val = qgetenv(varName: var.toLatin1().data());
1197 if (val.isNull()) {
1198 location.fatal(QStringLiteral("Environment variable '%1' undefined")
1199 .arg(a: var));
1200 } else {
1201 includeFile += QString::fromLatin1(ba: val);
1202 }
1203 }
1204 } else {
1205 includeFile += c;
1206 SKIP_CHAR();
1207 }
1208 }
1209 SKIP_SPACES();
1210 if (cc != ')')
1211 location.fatal(QStringLiteral("Bad include syntax"));
1212 SKIP_CHAR();
1213 SKIP_SPACES();
1214 if (cc != '#' && cc != '\n')
1215 location.fatal(QStringLiteral("Trailing garbage"));
1216
1217 /*
1218 Here is the recursive call.
1219 */
1220 load(location, fileName: QFileInfo(QDir(m_workingDirs.top()), includeFile).filePath());
1221 } else {
1222 /*
1223 It wasn't an include statement, so it's something else.
1224 We must see either '=' or '+=' next. If not, fatal error.
1225 */
1226 if (cc == '+') {
1227 plus = true;
1228 SKIP_CHAR();
1229 }
1230 if (cc != '=')
1231 location.fatal(QStringLiteral("Expected '=' or '+=' after key"));
1232 SKIP_CHAR();
1233 SKIP_SPACES();
1234
1235 for (;;) {
1236 if (cc == '\\') {
1237 qsizetype metaCharPos;
1238
1239 SKIP_CHAR();
1240 if (cc == '\n') {
1241 SKIP_CHAR();
1242 } else if (cc > '0' && cc < '8') {
1243 word += QChar(c.digitValue());
1244 SKIP_CHAR();
1245 } else if ((metaCharPos = QString::fromLatin1(ba: "abfnrtv").indexOf(ch: c))
1246 != -1) {
1247 word += QLatin1Char("\a\b\f\n\r\t\v"[metaCharPos]);
1248 SKIP_CHAR();
1249 } else {
1250 PUT_CHAR();
1251 }
1252 } else if (c.isSpace() || cc == '#') {
1253 if (inQuote) {
1254 if (cc == '\n')
1255 location.fatal(QStringLiteral("Unterminated string"));
1256 PUT_CHAR();
1257 } else {
1258 if (!word.isEmpty() || needsExpansion) {
1259 rhsValues << word;
1260 word.clear();
1261 needsExpansion = false;
1262 }
1263 if (cc == '\n' || cc == '#')
1264 break;
1265 SKIP_SPACES();
1266 }
1267 } else if (cc == '"') {
1268 if (inQuote) {
1269 if (!word.isEmpty() || needsExpansion)
1270 rhsValues << word;
1271 word.clear();
1272 needsExpansion = false;
1273 }
1274 inQuote = !inQuote;
1275 SKIP_CHAR();
1276 } else if (cc == '$') {
1277 QString var;
1278 QChar delim(' ');
1279 bool braces = false;
1280 SKIP_CHAR();
1281 if (cc == '{') {
1282 SKIP_CHAR();
1283 braces = true;
1284 }
1285 while (c.isLetterOrNumber() || cc == '_') {
1286 var += c;
1287 SKIP_CHAR();
1288 }
1289 if (braces) {
1290 if (cc == ',') {
1291 SKIP_CHAR();
1292 delim = c;
1293 SKIP_CHAR();
1294 }
1295 if (cc == '}')
1296 SKIP_CHAR();
1297 else if (delim == '}')
1298 delim = QChar(); // null delimiter
1299 else
1300 location.fatal(QStringLiteral("Missing '}'"));
1301 }
1302 if (!var.isEmpty()) {
1303 const QByteArray val = qgetenv(varName: var.toLatin1().constData());
1304 if (val.isNull()) {
1305 expandVars << ExpandVar(rhsValues.size(), word.size(), std::move(var), delim);
1306 needsExpansion = true;
1307 } else if (braces) { // ${VAR} inserts content from an env. variable for processing
1308 text.insert(i, s: QString::fromLatin1(ba: val));
1309 c = text.at(i);
1310 cc = c.unicode();
1311 } else { // while $VAR simply reads the value and stores it to a config variable.
1312 word += QString::fromLatin1(ba: val);
1313 }
1314 }
1315 } else {
1316 if (!inQuote && cc == '=')
1317 location.fatal(QStringLiteral("Unexpected '='"));
1318 PUT_CHAR();
1319 }
1320 }
1321 for (const auto &key : keys) {
1322 if (!keySyntax.match(subject: key).hasMatch())
1323 keyLoc.fatal(QStringLiteral("Invalid key '%1'").arg(a: key));
1324
1325 ConfigVar configVar(key, rhsValues, QDir::currentPath(), keyLoc, expandVars);
1326 if (plus && m_configVars.contains(key)) {
1327 m_configVars[key].append(other: configVar);
1328 } else {
1329 m_configVars.insert(key, value: configVar);
1330 }
1331 }
1332 }
1333 } else {
1334 location.fatal(QStringLiteral("Unexpected character '%1' at beginning of line").arg(a: c));
1335 }
1336 }
1337 popWorkingDir();
1338
1339#undef SKIP_CHAR
1340#undef SKIP_SPACES
1341#undef PUT_CHAR
1342}
1343
1344bool Config::isFileExcluded(const QString &fileName, const QSet<QString> &excludedFiles)
1345{
1346 for (const QString &entry : excludedFiles) {
1347 if (entry.contains(c: QLatin1Char('*')) || entry.contains(c: QLatin1Char('?'))) {
1348 QRegularExpression re(QRegularExpression::wildcardToRegularExpression(str: entry));
1349 if (re.match(subject: fileName).hasMatch())
1350 return true;
1351 }
1352 }
1353 return excludedFiles.contains(value: fileName);
1354}
1355
1356QStringList Config::getFilesHere(const QString &uncleanDir, const QString &nameFilter,
1357 const Location &location, const QSet<QString> &excludedDirs,
1358 const QSet<QString> &excludedFiles)
1359{
1360 // TODO: Understand why location is used to branch the
1361 // canonicalization and why the two different methods are used.
1362 QString dir =
1363 location.isEmpty() ? QDir::cleanPath(path: uncleanDir) : QDir(uncleanDir).canonicalPath();
1364 QStringList result;
1365 if (excludedDirs.contains(value: dir))
1366 return result;
1367
1368 QDir dirInfo(dir);
1369
1370 dirInfo.setNameFilters(nameFilter.split(sep: QLatin1Char(' ')));
1371 dirInfo.setSorting(QDir::Name);
1372 dirInfo.setFilter(QDir::Files);
1373 QStringList fileNames = dirInfo.entryList();
1374 for (const auto &file : std::as_const(t&: fileNames)) {
1375 // TODO: Understand if this is needed and, should it be, if it
1376 // is indeed the only case that should be considered.
1377 if (!file.startsWith(c: QLatin1Char('~'))) {
1378 QString s = dirInfo.filePath(fileName: file);
1379 QString c = QDir::cleanPath(path: s);
1380 if (!isFileExcluded(fileName: c, excludedFiles))
1381 result.append(t: c);
1382 }
1383 }
1384
1385 dirInfo.setNameFilters(QStringList(QLatin1String("*")));
1386 dirInfo.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
1387 fileNames = dirInfo.entryList();
1388 for (const auto &file : fileNames)
1389 result += getFilesHere(uncleanDir: dirInfo.filePath(fileName: file), nameFilter, location, excludedDirs,
1390 excludedFiles);
1391 return result;
1392}
1393
1394/*!
1395 Set \a dir as the working directory and push it onto the
1396 stack of working directories.
1397 */
1398void Config::pushWorkingDir(const QString &dir)
1399{
1400 m_workingDirs.push(t: dir);
1401 QDir::setCurrent(dir);
1402}
1403
1404/*!
1405 Pop the top entry from the stack of working directories.
1406 Set the working directory to the next one on the stack,
1407 if one exists.
1408 */
1409void Config::popWorkingDir()
1410{
1411 Q_ASSERT(!m_workingDirs.isEmpty());
1412 m_workingDirs.pop();
1413 if (!m_workingDirs.isEmpty())
1414 QDir::setCurrent(m_workingDirs.top());
1415}
1416
1417const Config::ExcludedPaths& Config::getExcludedPaths() {
1418 if (m_excludedPaths)
1419 return *m_excludedPaths;
1420
1421 const auto &excludedDirList = getCanonicalPathList(CONFIG_EXCLUDEDIRS);
1422 const auto &excludedFilesList = getCanonicalPathList(CONFIG_EXCLUDEFILES);
1423
1424 QSet<QString> excludedDirs = QSet<QString>(excludedDirList.cbegin(), excludedDirList.cend());
1425 QSet<QString> excludedFiles = QSet<QString>(excludedFilesList.cbegin(), excludedFilesList.cend());
1426
1427 m_excludedPaths.emplace(args: ExcludedPaths{.excluded_directories: std::move(excludedDirs), .excluded_files: std::move(excludedFiles)});
1428
1429 return *m_excludedPaths;
1430}
1431
1432/*!
1433 Returns a SourceLink struct with settings required to
1434 construct source links to API entities.
1435*/
1436const Config::SourceLink &Config::getSourceLink()
1437{
1438 if (m_sourceLink)
1439 return *m_sourceLink;
1440
1441 const auto srcUrl{CONFIG_URL + Config::dot + CONFIG_SOURCES};
1442
1443 const auto baseUrl = m_configVars.value(key: srcUrl).asString();
1444 const auto rootPath = m_configVars.value(key: srcUrl + dot + CONFIG_ROOTDIR).asString();
1445 const auto linkText = m_configVars.value(key: srcUrl + dot + "linktext").asString();
1446 const auto enabled = m_configVars.value(key: srcUrl + dot + "enabled").asBool();
1447
1448 m_sourceLink.emplace(args: SourceLink{.baseUrl: baseUrl, .rootPath: rootPath, .linkText: linkText, .enabled: enabled});
1449 return *m_sourceLink;
1450}
1451
1452std::set<Config::HeaderFilePath> Config::getHeaderFiles() {
1453 static QStringList accepted_header_file_extensions{
1454 "ch", "h", "h++", "hh", "hpp", "hxx"
1455 };
1456
1457 const auto& [excludedDirs, excludedFiles] = getExcludedPaths();
1458
1459 QStringList headerList =
1460 getAllFiles(CONFIG_HEADERS, CONFIG_HEADERDIRS, excludedDirs, excludedFiles);
1461
1462 std::set<HeaderFilePath> headers{};
1463
1464 for (const auto& header : headerList) {
1465 if (header.contains(s: "doc/snippets")) continue;
1466
1467 if (!accepted_header_file_extensions.contains(str: QFileInfo{header}.suffix()))
1468 continue;
1469
1470 headers.insert(x: HeaderFilePath{.path: QFileInfo{header}.canonicalPath(), .filename: QFileInfo{header}.fileName()});
1471 }
1472
1473 return headers;
1474}
1475
1476QT_END_NAMESPACE
1477

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