Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
outputconfiguration.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
5
6#include "config.h"
7
8#include <QtCore/qdir.h>
9#include <QtCore/qregularexpression.h>
10
11#include <optional>
12
13QT_BEGIN_NAMESPACE
14
15using namespace Qt::StringLiterals;
16
17/*!
18 \enum Pipeline
19 \internal
20 \inmodule QDoc
21 \relates OutputSelection
22 \brief Identifies an implementation that can own publication formats.
23
24 Format identity is independent of the implementation that renders it.
25 Selecting a pipeline does not require it to own any document format.
26
27 \value Legacy The compiled-in legacy generators.
28 \value Template The template engine with its effective package capabilities.
29*/
30
31/*!
32 \struct PipelineCapabilities
33 \internal
34 \inmodule QDoc
35 \brief Describes a pipeline without constructing generators or loading packages.
36
37 The caller supplies compiled-in legacy capabilities or the effective
38 template package's capabilities. Resolution reads these values without
39 retaining references to them. An unavailable selected pipeline is an error.
40
41 A declared but broken format remains an ownership claim even when it is
42 absent from \c{supportedFormats}. A broken template declaration therefore
43 prevents fallback to legacy. Extensions supply defaults only; they do not
44 determine format identity, ownership, or template selection.
45*/
46
47/*!
48 \variable PipelineCapabilities::pipeline
49 \brief The pipeline whose capabilities this record describes.
50*/
51
52/*!
53 \variable PipelineCapabilities::available
54 \brief Whether this pipeline can run in the invoking build and environment.
55
56 Set this to \c false for a compiled-out engine or an unusable package.
57*/
58
59/*!
60 \variable PipelineCapabilities::supportedFormats
61 \brief The case-sensitive format identities this pipeline can render.
62*/
63
64/*!
65 \variable PipelineCapabilities::brokenFormats
66 \brief The declared formats whose requirements the pipeline cannot satisfy.
67
68 These identities need not also appear in \c{supportedFormats}. A requested
69 broken declaration is an error, not permission to choose another owner.
70*/
71
72/*!
73 \variable PipelineCapabilities::formatExtensions
74 \brief The pipeline or effective-package extension defaults, keyed by format.
75
76 Resolution applies an explicit \c{<Format>.extension} override. Each
77 requested format needs a non-empty effective extension; unfamiliar formats
78 do not inherit another format's extension.
79*/
80
81/*!
82 \struct OutputConfigurationInput
83 \internal
84 \inmodule QDoc
85 \brief Supplies invocation-specific selections and pipeline capabilities.
86
87 The caller supplies command-line values in their original order. A
88 non-empty list replaces the corresponding configuration selection; it is
89 not additive. Represent an explicit empty argument with an empty string,
90 rather than an absent list. These inputs do not expose a public option or
91 invoke package loading.
92*/
93
94/*!
95 \variable OutputConfigurationInput::cliPipelines
96 \brief The ordered command-line pipeline selection, or an empty list if absent.
97*/
98
99/*!
100 \variable OutputConfigurationInput::cliFormats
101 \brief The ordered command-line format selection, or an empty list if absent.
102
103 This selection may introduce formats absent from the configuration.
104*/
105
106/*!
107 \variable OutputConfigurationInput::pipelineCapabilities
108 \brief The capability records for pipelines known to the caller.
109
110 Supply one effective record per pipeline. Absence of a selected pipeline's
111 record is an error, as is a record whose \c{available} value is false.
112*/
113
114/*!
115 \struct OutputSelection
116 \internal
117 \inmodule QDoc
118 \brief Resolves format requests and ownership independently of destination layout.
119
120 Explicit pipeline presence in a configuration file or include, or an
121 explicit command-line selection, opts in to the new publication model.
122 Without either, the result is a compatibility marker; existing runtime
123 interpretation retains control.
124
125 The result retains the full configured format order, followed by CLI-only
126 identities. Repeated identities keep their first occurrence. Identity
127 tokens are trimmed and empty tokens are discarded; path values do not use
128 this normalization. Requested flags record CLI replacement without losing
129 configured positions needed by subsequent layout planning.
130
131 Template owns its declared formats before legacy, independently of pipeline
132 order. Ownership and extension values are usable for requested formats only
133 in New mode when hasErrors() returns \c false. Non-requested entries retain
134 configured identity and position without requiring a usable owner.
135
136 Resolution never creates output, constructs generators, loads packages, or
137 emits diagnostics. The caller handles errors before publication and emits
138 migration notices outside the authored-documentation warning budget.
139*/
140
141/*!
142 \enum OutputSelection::Mode
143 \internal
144 \brief Identifies which configuration interpretation controls publication.
145
146 \value Compatibility No explicit pipeline selection; use existing runtime behavior.
147 \value New Explicit pipeline selection; use the resolved publication model.
148*/
149
150/*!
151 \struct OutputSelection::Diagnostic
152 \internal
153 \inmodule QDoc
154 \brief Carries a source-positioned publication diagnostic without emitting it.
155
156 Resolution deduplicates equal severity, file, line, message, and details.
157 The caller owns presentation and failure handling; this record does not
158 increment QDoc's warning or error counters.
159*/
160
161/*!
162 \enum OutputSelection::Diagnostic::Kind
163 \internal
164 \brief Distinguishes blocking errors from migration advice.
165
166 \value Error The configuration cannot be published as resolved.
167 \value Notice Migration advice, not an authored-documentation warning.
168*/
169
170/*!
171 \variable OutputSelection::Diagnostic::kind
172 \brief The diagnostic's effect on publication validity.
173*/
174
175/*!
176 \variable OutputSelection::Diagnostic::location
177 \brief The responsible configuration declaration, if known.
178
179 CLI-only and programmatic selections can have an empty location.
180*/
181
182/*!
183 \variable OutputSelection::Diagnostic::message
184 \brief The primary explanation of the configuration problem or migration advice.
185*/
186
187/*!
188 \variable OutputSelection::Diagnostic::details
189 \brief Optional context to accompany the primary message.
190*/
191
192/*!
193 \struct OutputSelection::Format
194 \internal
195 \inmodule QDoc
196 \brief Records a format's identity, request status, owner, and extension.
197
198 A configured but unrequested entry remains in the result to preserve layout
199 assignment. Consumers must not mistake it for an additional publication.
200*/
201
202/*!
203 \variable OutputSelection::Format::format
204 \brief The case-sensitive permanent publication identity.
205*/
206
207/*!
208 \variable OutputSelection::Format::requested
209 \brief Whether the effective invocation requests this format.
210*/
211
212/*!
213 \variable OutputSelection::Format::configured
214 \brief Whether the identity occurs in the configured format list, including defaults.
215*/
216
217/*!
218 \variable OutputSelection::Format::pipeline
219 \brief The selected owner, usable only for a valid requested format in New mode.
220*/
221
222/*!
223 \variable OutputSelection::Format::fileExtension
224 \brief The effective extension without an automatically added separator.
225
226 The caller combines this value with the document name. It does not select
227 templates or imply a publication destination.
228*/
229
230/*!
231 \variable OutputSelection::Format::location
232 \brief The format-list declaration from which this entry derives.
233
234 This is empty for a CLI-only identity or a programmatic declaration without
235 source provenance.
236*/
237
238/*!
239 \variable OutputSelection::mode
240 \brief The configuration interpretation selected for this invocation.
241*/
242
243/*!
244 \variable OutputSelection::pipelines
245 \brief The available enabled pipelines in selection order, without duplicates.
246
247 This order does not determine format ownership. An unavailable selection
248 contributes an error rather than an entry in this list.
249*/
250
251/*!
252 \variable OutputSelection::formats
253 \brief The configured formats followed by CLI-only formats, in first-occurrence order.
254*/
255
256/*!
257 \variable OutputSelection::diagnostics
258 \brief The deduplicated errors and notices collected during resolution.
259*/
260
261namespace {
262
263using Diagnostic = OutputSelection::Diagnostic;
264
265QString pipelineName(Pipeline pipeline)
266{
267 return pipeline == Pipeline::Legacy ? u"legacy"_s : u"template"_s;
268}
269
270std::optional<Pipeline> pipelineFromName(const QString &name)
271{
272 if (name == "legacy"_L1)
273 return Pipeline::Legacy;
274 if (name == "template"_L1)
275 return Pipeline::Template;
276 return std::nullopt;
277}
278
279QString configKey(const QString &format, const QString &suffix)
280{
281 return format + Config::dot + suffix;
282}
283
284bool isValidFormatIdentity(const QString &format)
285{
286 static const QRegularExpression identity(uR"(^[A-Za-z0-9_]+$)"_s);
287 return identity.match(format).hasMatch();
288}
289
290QStringList filterIdentityTokens(const QStringList &values)
291{
292 QStringList result;
293 for (const QString &value : values) {
294 const QString trimmed = value.trimmed();
295 if (!trimmed.isEmpty() && !result.contains(trimmed))
296 result.append(trimmed);
297 }
298 return result;
299}
300
301bool isValidModuleDir(const QString &project)
302{
303 static const QRegularExpression unsafe(uR"([/\<>:"|?*\x00-\x1F])"_s);
304 return !project.isEmpty() && project != "."_L1 && project != ".."_L1
305 && project == project.trimmed() && !project.endsWith(u'.')
306 && !unsafe.match(project).hasMatch();
307}
308
309std::optional<PublicationLayout> parsePublicationLayout(const QString &value)
310{
311 if (value == "flat"_L1)
313 if (value == "per-module"_L1)
315 return std::nullopt;
316}
317
318QString joinPath(const QString &base, const QString &sub)
319{
320 if (sub.isEmpty())
321 return base.isEmpty() ? u"."_s : QDir::cleanPath(base);
322 if (base.isEmpty())
323 return QDir::cleanPath(sub);
324 return QDir::cleanPath(base + u'/' + sub);
325}
326
327class DiagnosticReporter
328{
329public:
330 explicit DiagnosticReporter(QVector<Diagnostic> &diagnostics) : m_diagnostics(diagnostics)
331 {
332 for (const auto &diagnostic : diagnostics)
333 m_seen.insert(key(diagnostic));
334 }
335
336 void add(Diagnostic::Kind kind, const Location &location, QString message,
337 QString details = {})
338 {
339 Diagnostic diagnostic{kind, location, std::move(message), std::move(details)};
340 const QString identity = key(diagnostic);
341 if (m_seen.contains(identity))
342 return;
343 m_seen.insert(identity);
344 m_diagnostics.push_back(std::move(diagnostic));
345 }
346
347private:
348 static QString key(const Diagnostic &diagnostic)
349 {
350 return QString::number(static_cast<int>(diagnostic.kind)) + u'\0'
351 + diagnostic.location.filePath() + u'\0'
352 + QString::number(diagnostic.location.lineNo()) + u'\0'
353 + diagnostic.message + u'\0' + diagnostic.details;
354 }
355
356 QVector<Diagnostic> &m_diagnostics;
357 QSet<QString> m_seen;
358};
359
360} // namespace
361
362/*!
363 Resolves publication requests from \a config and the explicit \a input.
364
365 Returns diagnostics as data and leaves both inputs unchanged. Compatibility
366 mode returns no format plan but can carry notices for new-only settings
367 without explicit pipeline opt-in. In New mode, errors invalidate the result;
368 callers must check hasErrors() before using requested owners or extensions.
369*/
371 const OutputConfigurationInput &input)
372{
373 OutputSelection result;
374 DiagnosticReporter reporter(result.diagnostics);
375 const bool cliPipelines = !input.cliPipelines.isEmpty();
376 if (!cliPipelines && !config.contains(CONFIG_PIPELINE)) {
377 const auto flagNewOnlySetting = [&](const QString &var) {
378 reporter.add(Diagnostic::Kind::Notice, config.get(var).location(),
379 "New publication setting '%1' has no effect without an explicit "
380 "'pipeline' selection."_L1.arg(var));
381 };
382 if (config.contains(CONFIG_PUBLICATIONLAYOUT))
383 flagNewOnlySetting(CONFIG_PUBLICATIONLAYOUT);
384 if (config.contains(CONFIG_TEMPLATEPACKAGE))
385 flagNewOnlySetting(CONFIG_TEMPLATEPACKAGE);
386 for (const QString &key : config.variableNames()) {
387 if (key.endsWith(Config::dot + CONFIG_PUBLICATIONLAYOUT))
388 flagNewOnlySetting(key);
389 }
390 return result;
391 }
392 result.mode = Mode::New;
393
394 const PipelineCapabilities *legacy = nullptr;
395 const PipelineCapabilities *templated = nullptr;
396 for (const auto &capability : input.pipelineCapabilities) {
397 if (capability.pipeline == Pipeline::Legacy)
398 legacy = &capability;
399 else
400 templated = &capability;
401 }
402
403 const auto &pipelineVar = config.get(CONFIG_PIPELINE);
404 const Location pipelineSource = cliPipelines ? Location() : pipelineVar.location();
405 const auto selected = filterIdentityTokens(cliPipelines ? input.cliPipelines
406 : pipelineVar.asStringList());
407 if (selected.isEmpty())
408 reporter.add(Diagnostic::Kind::Error, pipelineSource, u"Pipeline selection is empty."_s,
409 u"Select at least one pipeline in the configuration or on the command line."_s);
410 for (const QString &name : selected) {
411 const auto pipeline = pipelineFromName(name);
412 if (!pipeline) {
413 reporter.add(Diagnostic::Kind::Error, pipelineSource,
414 "Pipeline '%1' is not recognized."_L1.arg(name),
415 u"QDoc supports the 'legacy' and 'template' pipelines."_s);
416 continue;
417 }
418 const auto *capability = *pipeline == Pipeline::Legacy ? legacy : templated;
419 if (!capability || !capability->available) {
420 reporter.add(Diagnostic::Kind::Error, pipelineSource,
421 "Pipeline '%1' is unavailable."_L1.arg(name));
422 continue;
423 }
424 result.pipelines.push_back(*pipeline);
425 }
426
427 const auto &formatsVar = config.get(CONFIG_OUTPUTFORMATS);
428 const auto configured = filterIdentityTokens(formatsVar.asStringList());
429 const auto requested = input.cliFormats.isEmpty() ? configured
430 : filterIdentityTokens(input.cliFormats);
431 if (requested.isEmpty())
432 reporter.add(Diagnostic::Kind::Error,
433 input.cliFormats.isEmpty() ? formatsVar.location() : Location(),
434 u"Format selection is empty."_s,
435 u"Select at least one output format in the configuration or on the command line."_s);
436
437 const QSet<QString> requestedSet(requested.cbegin(), requested.cend());
438 const bool templateEnabled = result.pipelines.contains(Pipeline::Template);
439 const bool legacyEnabled = result.pipelines.contains(Pipeline::Legacy);
440 const auto declares = [](const PipelineCapabilities *capability, const QString &format) {
441 return capability && (capability->supportedFormats.contains(format)
442 || capability->brokenFormats.contains(format));
443 };
444 const auto addFormat = [&](const QString &format, const Location &location, bool configured) {
445 Format entry;
446 entry.format = format;
447 entry.requested = requestedSet.contains(format);
448 entry.configured = configured;
449 entry.location = location;
450 if (!isValidFormatIdentity(format)) {
451 reporter.add(Diagnostic::Kind::Error, location,
452 "Invalid format identity '%1'."_L1.arg(format),
453 u"Format names may contain letters, digits, and underscores only."_s);
454 }
455
456 const PipelineCapabilities *owner = nullptr;
457 if (templateEnabled && declares(templated, format))
458 owner = templated;
459 else if (legacyEnabled && declares(legacy, format))
460 owner = legacy;
461
462 if (owner) {
463 entry.pipeline = owner->pipeline;
464 entry.fileExtension = owner->formatExtensions.value(format);
465 if (entry.requested && owner->brokenFormats.contains(format)) {
466 reporter.add(Diagnostic::Kind::Error, location,
467 "Format '%1' is declared by pipeline '%2' but the declaration "
468 "is broken."_L1.arg(format, pipelineName(owner->pipeline)),
469 u"QDoc does not fall back to another pipeline for a broken declaration."_s);
470 } else {
471 const QString extensionKey = configKey(format, u"extension"_s);
472 const auto &extension = config.get(extensionKey);
473 const bool overridden = config.contains(extensionKey);
474 if (overridden) {
475 const auto values = extension.asStringList();
476 entry.fileExtension = values.value(0);
477 if (entry.requested && values.size() > 1)
478 reporter.add(Diagnostic::Kind::Error, extension.location(),
479 "%1 must contain a single value."_L1.arg(extensionKey));
480 }
481 if (entry.requested && entry.fileExtension.isEmpty())
482 reporter.add(Diagnostic::Kind::Error,
483 overridden ? extension.location() : location,
484 "Format '%1' has no file extension."_L1.arg(format),
485 "Provide a pipeline default or a non-empty %1."_L1.arg(extensionKey));
486 }
487 } else if (entry.requested) {
488 QStringList names;
489 for (Pipeline pipeline : result.pipelines)
490 names.append(pipelineName(pipeline));
491 reporter.add(Diagnostic::Kind::Error, location,
492 "Format '%1' is not supported by any selected pipeline."_L1.arg(format),
493 names.isEmpty() ? u"No pipeline is available."_s
494 : "Selected pipelines: %1."_L1.arg(names.join(u", "_s)));
495 }
496 result.formats.push_back(std::move(entry));
497 };
498
499 for (const QString &format : configured)
500 addFormat(format, formatsVar.location(), true);
501 for (const QString &format : requested) {
502 if (!configured.contains(format))
503 addFormat(format, Location(), false);
504 }
505 return result;
506}
507
508/*!
509 Returns \c true if any diagnostic blocks publication. Notices alone do not
510 invalidate the selection or consume the documentation warning budget.
511*/
513{
514 for (const auto &diagnostic : diagnostics) {
515 if (diagnostic.kind == Diagnostic::Kind::Error)
516 return true;
517 }
518 return false;
519}
520
521/*!
522 Returns the entry for the case-sensitive identity \a format, or \c{nullptr}
523 if it is absent. The entry may be configured but unrequested.
524
525 The result borrows storage from this selection and is valid only while that
526 storage remains alive and unchanged. Looking up an entry does not validate it.
527*/
528const OutputSelection::Format *OutputSelection::findFormat(const QString &format) const
529{
530 for (const auto &entry : formats) {
531 if (entry.format == format)
532 return &entry;
533 }
534 return nullptr;
535}
536
537/*!
538 \enum PublicationLayout
539 \internal
540 \inmodule QDoc
541 \relates OutputConfiguration
542 \brief Determines whether a format's publication tree has a module directory.
543
544 Layout is independent of execution mode and format directory selection.
545
546 \value Flat Modules can share the format directory. Image and style
547 subdirectories remain possible.
548 \value PerModule Append the lower-case project identity to the format directory.
549*/
550
551/*!
552 \struct OutputConfiguration
553 \internal
554 \inmodule QDoc
555 \brief Adds publication destinations to a resolved output selection.
556
557 Resolution consumes OutputSelection without constructing generators, parsing
558 source, creating output, or emitting diagnostics. Compatibility mode remains
559 a marker for the unchanged legacy interpretation, not a new layout plan.
560
561 The first configured format defaults to the publication root. Later
562 configured formats and CLI-only formats default to lower-case format names.
563 Explicit \c{<Format>.outputsubdir} values apply literally, including empty
564 and \c{../} values. Replacing requested formats or moving the first format
565 does not promote another configured format into the root.
566
567 Layout precedence is \c{<Format>.publicationlayout}, \c{publicationlayout},
568 the explicit deprecated \c{<Format>.nosubdirs} alias, then Flat. The alias
569 maps true to Flat and false, including an empty declaration, to PerModule.
570 Invalid or empty new layout values are errors, not a request to use an
571 inherited alias. PerModule requires a safe module directory component.
572
573 Equal normalized destinations for requested formats are errors; nested
574 trees are allowed. Before writing, consumers must also validate filesystem
575 aliases and case, reserved subtrees, and actual artifact ownership. Lexical
576 planning neither performs those checks nor authorizes filesystem writes.
577*/
578
579/*!
580 \typealias OutputConfiguration::Mode
581 \internal
582 \brief Uses the same interpretation modes as OutputSelection.
583*/
584
585/*!
586 \typealias OutputConfiguration::Diagnostic
587 \internal
588 \brief Uses the same diagnostic data and severity contract as OutputSelection.
589*/
590
591/*!
592 \struct OutputConfiguration::FormatPlan
593 \internal
594 \inmodule QDoc
595 \brief Extends a format selection with its publication directory and layout.
596
597 The inherited request and ownership fields retain their OutputSelection
598 meaning. A non-requested entry preserves configured assignment but is not
599 another publication. Check the containing configuration's errors before
600 using a requested plan.
601*/
602
603/*!
604 \variable OutputConfiguration::FormatPlan::formatDir
605 \brief The literal or default format directory relative to the publication root.
606
607 Empty means the root. Parent components are deliberate, not an instruction
608 to rewrite the destination to remain under the root.
609*/
610
611/*!
612 \variable OutputConfiguration::FormatPlan::formatDirLocation
613 \brief The directory declaration, or the format-list origin for a default directory.
614
615 CLI-only defaults and programmatic declarations can have an empty location.
616*/
617
618/*!
619 \variable OutputConfiguration::FormatPlan::layout
620 \brief The resolved policy for adding a module directory.
621*/
622
623/*!
624 \variable OutputConfiguration::FormatPlan::layoutLocation
625 \brief The effective layout or deprecated-alias declaration, if one applies.
626
627 This is empty for the implicit Flat default or absent source provenance.
628*/
629
630/*!
631 \variable OutputConfiguration::FormatPlan::moduleDir
632 \brief The lower-case project directory for PerModule, or empty for Flat.
633
634 An invalid PerModule identity leaves this empty and produces an error for
635 a requested format. It must not be interpreted as a valid Flat fallback.
636*/
637
638/*!
639 \variable OutputConfiguration::mode
640 \brief The interpretation mode inherited from output selection.
641*/
642
643/*!
644 \variable OutputConfiguration::pipelines
645 \brief The available enabled pipelines inherited from output selection.
646*/
647
648/*!
649 \variable OutputConfiguration::root
650 \brief The publication root before complete-path normalization.
651
652 The invocation's output-directory override wins over configured
653 \c{outputdir}. Empty means the current directory. The override replaces only
654 the root, not the format or module layout.
655*/
656
657/*!
658 \variable OutputConfiguration::formats
659 \brief The configured and CLI-only format plans in selection order.
660
661 Only entries whose inherited \c{requested} value is true claim destinations.
662*/
663
664/*!
665 \variable OutputConfiguration::diagnostics
666 \brief The combined, deduplicated selection and layout diagnostics.
667
668 These records have no emission or warning-budget side effects.
669*/
670
671/*!
672 Resolves selection and destination layout from \a config and \a input.
673
674 Also reads the invocation's output-directory override from Config. Returns
675 a compatibility marker without destinations when the new model is not
676 selected. Errors remain data; the caller must reject an invalid plan before
677 any publication writes.
678*/
680 const OutputConfigurationInput &input)
681{
682 auto selection = OutputSelection::resolve(config, input);
683 OutputConfiguration result;
684 result.mode = selection.mode;
685 result.pipelines = std::move(selection.pipelines);
686 result.diagnostics = std::move(selection.diagnostics);
687 if (result.mode == Mode::Compatibility)
688 return result;
689
690 DiagnosticReporter reporter(result.diagnostics);
691 result.root = Config::overrideOutputDir.isNull() ? config.get(CONFIG_OUTPUTDIR).asString()
692 : Config::overrideOutputDir;
693 const auto &project = config.get(CONFIG_PROJECT);
694 const QString moduleDir = project.asString().toLower();
695 for (qsizetype i = 0; i < selection.formats.size(); ++i) {
696 FormatPlan plan;
697 static_cast<OutputSelection::Format &>(plan) = std::move(selection.formats[i]);
698 const QString subKey = configKey(plan.format, u"outputsubdir"_s);
699 if (config.contains(subKey)) {
700 const auto &sub = config.get(subKey);
701 const auto values = sub.asStringList();
702 if (values.size() > 1)
703 reporter.add(Diagnostic::Kind::Error, sub.location(),
704 "%1 must contain a single value."_L1.arg(subKey),
705 u"Use one path per format."_s);
706 plan.formatDir = values.value(0);
707 plan.formatDirLocation = sub.location();
708 } else {
709 plan.formatDir = plan.configured && i == 0 ? QString() : plan.format.toLower();
710 plan.formatDirLocation = plan.location;
711 }
712
713 const QString perKey = configKey(plan.format, CONFIG_PUBLICATIONLAYOUT);
714 const QString layoutKey = config.contains(perKey) ? perKey : CONFIG_PUBLICATIONLAYOUT;
715 if (config.contains(layoutKey)) {
716 const auto &value = config.get(layoutKey);
717 plan.layoutLocation = value.location();
718 if (const auto layout = parsePublicationLayout(value.asString())) {
719 plan.layout = *layout;
720 } else {
721 reporter.add(Diagnostic::Kind::Error, value.location(),
722 "Invalid %1 '%2'."_L1.arg(layoutKey, value.asString()),
723 u"Expected 'flat' or 'per-module'."_s);
724 }
725 } else {
726 const QString aliasKey = configKey(plan.format, u"nosubdirs"_s);
727 if (config.contains(aliasKey)) {
728 const auto &alias = config.get(aliasKey);
730 plan.layoutLocation = alias.location();
731 reporter.add(Diagnostic::Kind::Notice, alias.location(),
732 "%1 is deprecated; use %2 or publicationlayout instead."_L1
733 .arg(aliasKey, perKey));
734 }
735 }
737 if (isValidModuleDir(moduleDir)) {
738 plan.moduleDir = moduleDir;
739 } else if (plan.requested) {
740 reporter.add(Diagnostic::Kind::Error, project.location(),
741 "Invalid project identity '%1' for per-module publication."_L1
742 .arg(project.asString()),
743 u"Project must be a safe directory name."_s);
744 }
745 }
746 result.formats.push_back(std::move(plan));
747 }
748
749 QHash<QString, const FormatPlan *> destinations;
750 for (const auto &plan : result.formats) {
751 if (!plan.requested)
752 continue;
753 const QString dir = result.destination(plan);
754 const auto it = destinations.constFind(dir);
755 if (it == destinations.constEnd()) {
756 destinations.insert(dir, &plan);
757 } else {
758 const auto &otherLocation = it.value()->formatDirLocation;
759 reporter.add(Diagnostic::Kind::Error, plan.formatDirLocation,
760 "Conflicting output destinations for '%1' and '%2': %3"_L1
761 .arg(it.value()->format, plan.format, dir),
762 otherLocation.isEmpty() ? QString()
763 : "Other destination declared at %1."_L1.arg(otherLocation.toString()));
764 }
765 }
766 return result;
767}
768
769/*!
770 Returns \c true if a selection or layout diagnostic blocks publication.
771 Migration notices alone do not invalidate the configuration.
772*/
774{
775 for (const auto &diagnostic : diagnostics) {
776 if (diagnostic.kind == Diagnostic::Kind::Error)
777 return true;
778 }
779 return false;
780}
781
782/*!
783 Returns the plan for the case-sensitive identity \a format, or \c{nullptr}
784 if absent. A configured entry can be present without being requested.
785
786 The pointer borrows storage from this configuration and remains valid only
787 while that storage is alive and unchanged. It does not certify plan validity.
788*/
790{
791 for (const auto &plan : formats) {
792 if (plan.format == format)
793 return &plan;
794 }
795 return nullptr;
796}
797
798/*!
799 Returns the normalized root, format directory, and module directory for
800 \a plan. Use an entry from this configuration after checking hasErrors().
801
802 A root-only path is normalized too; \c{.} denotes the current directory.
803 This query does not create directories, require their existence, resolve
804 filesystem aliases, or check whether the entry is requested.
805*/
807{
808 return joinPath(root, joinPath(plan.formatDir, plan.moduleDir));
809}
810
811QT_END_NAMESPACE
The Config class contains the configuration variables for controlling how qdoc produces documentation...
Definition config.h:95
The Location class provides a way to mark a location in a file.
Definition location.h:20
Location()
Constructs an empty location.
Definition location.cpp:50
#define CONFIG_PIPELINE
Definition config.h:441
#define CONFIG_OUTPUTDIR
Definition config.h:436
#define CONFIG_TEMPLATEPACKAGE
Definition config.h:464
#define CONFIG_PROJECT
Definition config.h:444
#define CONFIG_PUBLICATIONLAYOUT
Definition config.h:446
#define CONFIG_OUTPUTFORMATS
Definition config.h:437
PublicationLayout
\variable PipelineCapabilities::pipeline
\typealias OutputConfiguration::Mode
Adds publication destinations to a resolved output selection.
OutputSelection::Diagnostic Diagnostic
const FormatPlan * findFormat(const QString &format) const
Returns the plan for the case-sensitive identity format, or {nullptr} if absent.
bool hasErrors() const
Returns true if a selection or layout diagnostic blocks publication.
static OutputConfiguration resolve(const Config &config, const OutputConfigurationInput &input={})
\variable OutputConfiguration::FormatPlan::formatDir
QString destination(const FormatPlan &plan) const
Returns the normalized root, format directory, and module directory for plan.
OutputSelection::Mode Mode
Carries a source-positioned publication diagnostic without emitting it.
Kind
Distinguishes blocking errors from migration advice.
\variable OutputSelection::Diagnostic::kind
\variable OutputConfigurationInput::cliPipelines
bool hasErrors() const
Returns true if any diagnostic blocks publication.
Mode
Identifies which configuration interpretation controls publication.
static OutputSelection resolve(const Config &config, const OutputConfigurationInput &input={})
Resolves publication requests from config and the explicit input.
const Format * findFormat(const QString &format) const
Returns the entry for the case-sensitive identity format, or {nullptr} if it is absent.
Describes a pipeline without constructing generators or loading packages.