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 | |
14 | QT_BEGIN_NAMESPACE |
15 | |
16 | QString ConfigStrings::AUTOLINKERRORS = QStringLiteral("autolinkerrors" ); |
17 | QString ConfigStrings::BUILDVERSION = QStringLiteral("buildversion" ); |
18 | QString ConfigStrings::CLANGDEFINES = QStringLiteral("clangdefines" ); |
19 | QString ConfigStrings::CODEINDENT = QStringLiteral("codeindent" ); |
20 | QString ConfigStrings::CODEPREFIX = QStringLiteral("codeprefix" ); |
21 | QString ConfigStrings::CODESUFFIX = QStringLiteral("codesuffix" ); |
22 | QString ConfigStrings::CPPCLASSESPAGE = QStringLiteral("cppclassespage" ); |
23 | QString ConfigStrings::CPPCLASSESTITLE = QStringLiteral("cppclassestitle" ); |
24 | QString ConfigStrings::DEFINES = QStringLiteral("defines" ); |
25 | QString ConfigStrings::DEPENDS = QStringLiteral("depends" ); |
26 | QString ConfigStrings::DESCRIPTION = QStringLiteral("description" ); |
27 | QString ConfigStrings::DOCBOOKEXTENSIONS = QStringLiteral("usedocbookextensions" ); |
28 | QString ConfigStrings:: = QStringLiteral("endheader" ); |
29 | QString ConfigStrings::EXAMPLEDIRS = QStringLiteral("exampledirs" ); |
30 | QString ConfigStrings::EXAMPLES = QStringLiteral("examples" ); |
31 | QString ConfigStrings::EXAMPLESINSTALLPATH = QStringLiteral("examplesinstallpath" ); |
32 | QString ConfigStrings::EXCLUDEDIRS = QStringLiteral("excludedirs" ); |
33 | QString ConfigStrings::EXCLUDEFILES = QStringLiteral("excludefiles" ); |
34 | QString ConfigStrings:: = QStringLiteral("extraimages" ); |
35 | QString ConfigStrings::FALSEHOODS = QStringLiteral("falsehoods" ); |
36 | QString ConfigStrings::FORMATTING = QStringLiteral("formatting" ); |
37 | QString ConfigStrings:: = QStringLiteral("headerdirs" ); |
38 | QString ConfigStrings:: = QStringLiteral("headers" ); |
39 | QString ConfigStrings:: = QStringLiteral("headerscripts" ); |
40 | QString ConfigStrings:: = QStringLiteral("headerstyles" ); |
41 | QString ConfigStrings::HOMEPAGE = QStringLiteral("homepage" ); |
42 | QString ConfigStrings::HOMETITLE = QStringLiteral("hometitle" ); |
43 | QString ConfigStrings::IGNOREDIRECTIVES = QStringLiteral("ignoredirectives" ); |
44 | QString ConfigStrings::IGNORESINCE = QStringLiteral("ignoresince" ); |
45 | QString ConfigStrings::IGNORETOKENS = QStringLiteral("ignoretokens" ); |
46 | QString ConfigStrings::IGNOREWORDS = QStringLiteral("ignorewords" ); |
47 | QString ConfigStrings::IMAGEDIRS = QStringLiteral("imagedirs" ); |
48 | QString ConfigStrings::IMAGES = QStringLiteral("images" ); |
49 | QString ConfigStrings::INCLUDEPATHS = QStringLiteral("includepaths" ); |
50 | QString ConfigStrings::INCLUSIVE = QStringLiteral("inclusive" ); |
51 | QString ConfigStrings::INDEXES = QStringLiteral("indexes" ); |
52 | QString ConfigStrings::LANDINGPAGE = QStringLiteral("landingpage" ); |
53 | QString ConfigStrings::LANDINGTITLE = QStringLiteral("landingtitle" ); |
54 | QString ConfigStrings::LANGUAGE = QStringLiteral("language" ); |
55 | QString ConfigStrings::LOCATIONINFO = QStringLiteral("locationinfo" ); |
56 | QString ConfigStrings::LOGPROGRESS = QStringLiteral("logprogress" ); |
57 | QString ConfigStrings::MACRO = QStringLiteral("macro" ); |
58 | QString ConfigStrings::MANIFESTMETA = QStringLiteral("manifestmeta" ); |
59 | QString ConfigStrings:: = QStringLiteral("moduleheader" ); |
60 | QString ConfigStrings::NATURALLANGUAGE = QStringLiteral("naturallanguage" ); |
61 | QString ConfigStrings::NAVIGATION = QStringLiteral("navigation" ); |
62 | QString ConfigStrings::NOLINKERRORS = QStringLiteral("nolinkerrors" ); |
63 | QString ConfigStrings::OUTPUTDIR = QStringLiteral("outputdir" ); |
64 | QString ConfigStrings::OUTPUTFORMATS = QStringLiteral("outputformats" ); |
65 | QString ConfigStrings::OUTPUTPREFIXES = QStringLiteral("outputprefixes" ); |
66 | QString ConfigStrings::OUTPUTSUFFIXES = QStringLiteral("outputsuffixes" ); |
67 | QString ConfigStrings::PROJECT = QStringLiteral("project" ); |
68 | QString ConfigStrings::REDIRECTDOCUMENTATIONTODEVNULL = |
69 | QStringLiteral("redirectdocumentationtodevnull" ); |
70 | QString ConfigStrings::QHP = QStringLiteral("qhp" ); |
71 | QString ConfigStrings::QUOTINGINFORMATION = QStringLiteral("quotinginformation" ); |
72 | QString ConfigStrings::SCRIPTS = QStringLiteral("scripts" ); |
73 | QString ConfigStrings::SHOWINTERNAL = QStringLiteral("showinternal" ); |
74 | QString ConfigStrings::SINGLEEXEC = QStringLiteral("singleexec" ); |
75 | QString ConfigStrings::SOURCEDIRS = QStringLiteral("sourcedirs" ); |
76 | QString ConfigStrings::SOURCEENCODING = QStringLiteral("sourceencoding" ); |
77 | QString ConfigStrings::SOURCES = QStringLiteral("sources" ); |
78 | QString ConfigStrings::SPURIOUS = QStringLiteral("spurious" ); |
79 | QString ConfigStrings::STYLESHEETS = QStringLiteral("stylesheets" ); |
80 | QString ConfigStrings::SYNTAXHIGHLIGHTING = QStringLiteral("syntaxhighlighting" ); |
81 | QString ConfigStrings::TABSIZE = QStringLiteral("tabsize" ); |
82 | QString ConfigStrings::TAGFILE = QStringLiteral("tagfile" ); |
83 | QString ConfigStrings::TIMESTAMPS = QStringLiteral("timestamps" ); |
84 | QString ConfigStrings::TOCTITLES = QStringLiteral("toctitles" ); |
85 | QString ConfigStrings::URL = QStringLiteral("url" ); |
86 | QString ConfigStrings::VERSION = QStringLiteral("version" ); |
87 | QString ConfigStrings::VERSIONSYM = QStringLiteral("versionsym" ); |
88 | QString ConfigStrings::FILEEXTENSIONS = QStringLiteral("fileextensions" ); |
89 | QString ConfigStrings::IMAGEEXTENSIONS = QStringLiteral("imageextensions" ); |
90 | QString ConfigStrings::QMLTYPESPAGE = QStringLiteral("qmltypespage" ); |
91 | QString ConfigStrings::QMLTYPESTITLE = QStringLiteral("qmltypestitle" ); |
92 | QString ConfigStrings::WARNINGLIMIT = QStringLiteral("warninglimit" ); |
93 | |
94 | /*! |
95 | An entry in a stack, where each entry is a list |
96 | of string values. |
97 | */ |
98 | class MetaStackEntry |
99 | { |
100 | public: |
101 | void open(); |
102 | void close(); |
103 | |
104 | QStringList accum; |
105 | QStringList next; |
106 | }; |
107 | Q_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 | */ |
113 | void 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 | */ |
123 | void 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 | */ |
134 | class MetaStack : private QStack<MetaStackEntry> |
135 | { |
136 | public: |
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 | */ |
147 | MetaStack::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 | */ |
158 | void 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 | */ |
188 | QStringList 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 | |
197 | const QString Config::dot = QLatin1String("." ); |
198 | bool Config::m_debug = false; |
199 | bool Config::m_atomsDump = false; |
200 | bool Config::generateExamples = true; |
201 | QString Config::overrideOutputDir; |
202 | QString Config::installDir; |
203 | QSet<QString> Config::overrideOutputFormats; |
204 | QMap<QString, QString> Config::; |
205 | QStack<QString> Config::m_workingDirs; |
206 | QMap<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 | */ |
224 | QString 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 | */ |
241 | QStringList 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 | */ |
252 | QSet<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 | */ |
261 | bool 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 | */ |
274 | int 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 | */ |
290 | void 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 | */ |
334 | void Config::init(const QString &programName, const QStringList &args) |
335 | { |
336 | m_prog = programName; |
337 | processCommandLineOptions(args); |
338 | reset(); |
339 | } |
340 | |
341 | Config::~Config() |
342 | { |
343 | clear(); |
344 | } |
345 | |
346 | /*! |
347 | Clears the location and internal maps for config variables. |
348 | */ |
349 | void 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 | */ |
359 | void 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 | */ |
398 | void 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 | */ |
426 | void 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 | */ |
458 | void 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 | */ |
467 | void 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 | */ |
475 | void 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 | |
510 | void 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 | */ |
528 | void 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 | */ |
546 | QString 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 | */ |
572 | QSet<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 | */ |
601 | QStringList 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 ¤tPath = 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 | */ |
662 | QRegularExpression Config::getRegExp(const QString &var) const |
663 | { |
664 | QString pattern; |
665 | const auto subRegExps = getRegExpList(var); |
666 | |
667 | for (const auto ®Exp : 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 | */ |
684 | QList<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 | */ |
699 | QSet<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 | */ |
720 | QString 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 | */ |
758 | QStringList 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 | |
772 | QStringList 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 | |
784 | QStringList 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 | */ |
807 | QString 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 | */ |
856 | QString 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 = 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 | */ |
940 | QString 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 | */ |
1028 | int 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 | */ |
1043 | bool 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 | */ |
1053 | QStringList 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 | */ |
1086 | void 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 | |
1328 | bool 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 | |
1340 | QStringList 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 | */ |
1382 | void 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 | */ |
1393 | void 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 | |
1401 | QT_END_NAMESPACE |
1402 | |