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

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