git: 4573f1054f80 - main - devel/qt6-tools: Backport upstream patch to support LLVM 22
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Mon, 29 Jun 2026 15:14:00 UTC
The branch main has been updated by makc:
URL: https://cgit.FreeBSD.org/ports/commit/?id=4573f1054f80bffc4ca536c1ebba05e0cf4f1e52
commit 4573f1054f80bffc4ca536c1ebba05e0cf4f1e52
Author: Max Brazhnikov <makc@FreeBSD.org>
AuthorDate: 2026-06-29 15:13:50 +0000
Commit: Max Brazhnikov <makc@FreeBSD.org>
CommitDate: 2026-06-29 15:13:50 +0000
devel/qt6-tools: Backport upstream patch to support LLVM 22
PR: 295535
---
devel/qt6-tools/files/patch-QTBUG-144620 | 542 +++++++++++++++++++++++++++++++
1 file changed, 542 insertions(+)
diff --git a/devel/qt6-tools/files/patch-QTBUG-144620 b/devel/qt6-tools/files/patch-QTBUG-144620
new file mode 100644
index 000000000000..698fc7a90aa5
--- /dev/null
+++ b/devel/qt6-tools/files/patch-QTBUG-144620
@@ -0,0 +1,542 @@
+From f75f4090ed8e5c9dc898a313a5dd9b6878c2f034 Mon Sep 17 00:00:00 2001
+From: Paul Wicking <paul.wicking@qt.io>
+Date: Tue, 17 Mar 2026 14:36:28 +0100
+Subject: QDoc: Add LLVM 22 implementation to QualTypeNames fork
+
+LLVM 22 introduced sweeping changes to the Clang type system that break
+the existing QualTypeNames fork. NestedNameSpecifier changed from a
+pointer type to a value type, RecordType merged into TagType, and
+ElaboratedType merged into TypedefType and TagType through a new
+TypeWithKeyword base. These changes touch nearly every function in the
+fork, making incremental adaptation impractical.
+
+The fork now carries both the existing pre-22 implementation and a
+complete LLVM 22 implementation behind version guards. The LLVM 22 block
+is adapted from upstream clang/lib/AST/QualTypeNames.cpp (release/22.x)
+with three marked divergences: the random-specialization block in scope
+resolution is disabled to avoid unstable output, TypedefType qualifier
+resolution prefers the existing qualifier to preserve member type alias
+context, and the unhandled-type assertion is relaxed to a silent drop
+for types QDoc encounters that upstream doesn't anticipate.
+
+Both implementations are completely self-contained within their version
+guard to avoid subtle behavioral differences from shared code paths.
+
+Task-number: QTBUG-144620
+Change-Id: I36c4115b1c20f165d87a244b9aa852cb66352c99
+Reviewed-by: Topi Reinio <topi.reinio@qt.io>
+---
+ src/qdoc/qdoc/src/qdoc/clang/AST/QualTypeNames.h | 490 +++++++++++++++++++++++
+ 1 file changed, 490 insertions(+)
+
+diff --git a/src/qdoc/qdoc/src/qdoc/clang/AST/QualTypeNames.h b/src/qdoc/qdoc/src/qdoc/clang/AST/QualTypeNames.h
+index 977859911..f7440449e 100644
+--- ./src/qdoc/qdoc/src/qdoc/clang/AST/QualTypeNames.h
++++ ./src/qdoc/qdoc/src/qdoc/clang/AST/QualTypeNames.h
+@@ -36,6 +36,494 @@ namespace clang {
+
+ namespace TypeName {
+
++#if CLANG_VERSION_MAJOR >= 22
++
++// =========================================================================
++// LLVM 22+ implementation
++//
++// Adapted from upstream clang/lib/AST/QualTypeNames.cpp (release/22.x).
++// LLVM 22 changed NestedNameSpecifier from pointer to value type,
++// merged RecordType into TagType, and merged ElaboratedType into
++// TypedefType/TagType via TypeWithKeyword.
++//
++// QDoc divergences from upstream are marked with "QDoc divergence" comments.
++// =========================================================================
++
++inline QualType getFullyQualifiedType(QualType QT, const ASTContext &Ctx,
++ bool WithGlobalNsPrefix);
++
++static inline NestedNameSpecifier
++createNestedNameSpecifier(const ASTContext &Ctx, const NamespaceDecl *Namesp,
++ bool WithGlobalNsPrefix);
++
++static inline NestedNameSpecifier
++createNestedNameSpecifier(const ASTContext &Ctx, const TypeDecl *TD,
++ bool FullyQualify, bool WithGlobalNsPrefix);
++
++static inline NestedNameSpecifier
++createNestedNameSpecifierForScopeOf(const ASTContext &Ctx, const Decl *decl,
++ bool FullyQualified,
++ bool WithGlobalNsPrefix);
++
++static inline NestedNameSpecifier
++getFullyQualifiedNestedNameSpecifier(const ASTContext &Ctx,
++ NestedNameSpecifier NNS,
++ bool WithGlobalNsPrefix);
++
++static inline bool getFullyQualifiedTemplateName(const ASTContext &Ctx,
++ TemplateName &TName,
++ bool WithGlobalNsPrefix) {
++ bool Changed = false;
++ NestedNameSpecifier NNS = std::nullopt;
++
++ TemplateDecl *ArgTDecl = TName.getAsTemplateDecl();
++ if (!ArgTDecl) // ArgTDecl can be null in dependent contexts.
++ return false;
++
++ QualifiedTemplateName *QTName = TName.getAsQualifiedTemplateName();
++
++ if (QTName &&
++ !QTName->hasTemplateKeyword() &&
++ (NNS = QTName->getQualifier())) {
++ NestedNameSpecifier QNNS =
++ getFullyQualifiedNestedNameSpecifier(Ctx, NNS, WithGlobalNsPrefix);
++ if (QNNS != NNS) {
++ Changed = true;
++ NNS = QNNS;
++ } else {
++ NNS = std::nullopt;
++ }
++ } else {
++ NNS = createNestedNameSpecifierForScopeOf(
++ Ctx, ArgTDecl, true, WithGlobalNsPrefix);
++ }
++ if (NNS) {
++ TemplateName UnderlyingTN(ArgTDecl);
++ if (UsingShadowDecl *USD = TName.getAsUsingShadowDecl())
++ UnderlyingTN = TemplateName(USD);
++ TName =
++ Ctx.getQualifiedTemplateName(NNS,
++ /*TemplateKeyword=*/false, UnderlyingTN);
++ Changed = true;
++ }
++ return Changed;
++}
++
++static inline bool getFullyQualifiedTemplateArgument(const ASTContext &Ctx,
++ TemplateArgument &Arg,
++ bool WithGlobalNsPrefix) {
++ bool Changed = false;
++
++ // Note: we do not handle TemplateArgument::Expression, to replace it
++ // we need the information for the template instance decl.
++
++ if (Arg.getKind() == TemplateArgument::Template) {
++ TemplateName TName = Arg.getAsTemplate();
++ Changed = getFullyQualifiedTemplateName(Ctx, TName, WithGlobalNsPrefix);
++ if (Changed) {
++ Arg = TemplateArgument(TName);
++ }
++ } else if (Arg.getKind() == TemplateArgument::Type) {
++ QualType SubTy = Arg.getAsType();
++ // Check if the type needs more desugaring and recurse.
++ QualType QTFQ = getFullyQualifiedType(SubTy, Ctx, WithGlobalNsPrefix);
++ if (QTFQ != SubTy) {
++ Arg = TemplateArgument(QTFQ);
++ Changed = true;
++ }
++ }
++ return Changed;
++}
++
++static inline const Type *getFullyQualifiedTemplateType(
++ const ASTContext &Ctx,
++ const TagType *TSTRecord,
++ ElaboratedTypeKeyword Keyword,
++ NestedNameSpecifier Qualifier,
++ bool WithGlobalNsPrefix) {
++ // We are asked to fully qualify and we have a Record Type,
++ // which can point to a template instantiation with no sugar in any of
++ // its template argument, however we still need to fully qualify them.
++
++ const auto *TD = TSTRecord->getDecl();
++ const auto *TSTDecl = dyn_cast<ClassTemplateSpecializationDecl>(TD);
++ if (!TSTDecl)
++ return Ctx.getTagType(Keyword, Qualifier, TD, /*OwnsTag=*/false)
++ .getTypePtr();
++
++ const TemplateArgumentList &TemplateArgs = TSTDecl->getTemplateArgs();
++
++ bool MightHaveChanged = false;
++ SmallVector<TemplateArgument, 4> FQArgs;
++ for (unsigned int I = 0, E = TemplateArgs.size(); I != E; ++I) {
++ // cheap to copy and potentially modified by
++ // getFullyQualifedTemplateArgument
++ TemplateArgument Arg(TemplateArgs[I]);
++ MightHaveChanged |=
++ getFullyQualifiedTemplateArgument(Ctx, Arg, WithGlobalNsPrefix);
++ FQArgs.push_back(Arg);
++ }
++
++ if (!MightHaveChanged)
++ return Ctx.getTagType(Keyword, Qualifier, TD, /*OwnsTag=*/false)
++ .getTypePtr();
++ // If a fully qualified arg is different from the unqualified arg,
++ // allocate new type in the AST.
++ TemplateName TN = Ctx.getQualifiedTemplateName(
++ Qualifier, /*TemplateKeyword=*/false,
++ TemplateName(TSTDecl->getSpecializedTemplate()));
++ QualType QT = Ctx.getTemplateSpecializationType(
++ Keyword, TN, FQArgs,
++ /*CanonicalArgs=*/{}, TSTRecord->getCanonicalTypeInternal());
++ // getTemplateSpecializationType returns a fully qualified
++ // version of the specialization itself, so no need to qualify
++ // it.
++ return QT.getTypePtr();
++}
++
++static inline const Type *
++getFullyQualifiedTemplateType(const ASTContext &Ctx,
++ const TemplateSpecializationType *TST,
++ bool WithGlobalNsPrefix) {
++ TemplateName TName = TST->getTemplateName();
++ bool MightHaveChanged =
++ getFullyQualifiedTemplateName(Ctx, TName, WithGlobalNsPrefix);
++ SmallVector<TemplateArgument, 4> FQArgs;
++ // Cheap to copy and potentially modified by
++ // getFullyQualifedTemplateArgument.
++ for (TemplateArgument Arg : TST->template_arguments()) {
++ MightHaveChanged |=
++ getFullyQualifiedTemplateArgument(Ctx, Arg, WithGlobalNsPrefix);
++ FQArgs.push_back(Arg);
++ }
++
++ if (!MightHaveChanged)
++ return TST;
++
++ QualType NewQT =
++ Ctx.getTemplateSpecializationType(TST->getKeyword(), TName, FQArgs,
++ /*CanonicalArgs=*/{}, TST->desugar());
++ // getTemplateSpecializationType returns a fully qualified
++ // version of the specialization itself, so no need to qualify
++ // it.
++ return NewQT.getTypePtr();
++}
++
++static inline NestedNameSpecifier createOuterNNS(const ASTContext &Ctx,
++ const Decl *D,
++ bool FullyQualify,
++ bool WithGlobalNsPrefix) {
++ const DeclContext *DC = D->getDeclContext();
++ if (const auto *NS = dyn_cast<NamespaceDecl>(DC)) {
++ while (NS && NS->isInline()) {
++ // Ignore inline namespace;
++ NS = dyn_cast<NamespaceDecl>(NS->getDeclContext());
++ }
++ if (NS && NS->getDeclName()) {
++ return createNestedNameSpecifier(Ctx, NS, WithGlobalNsPrefix);
++ }
++ return std::nullopt; // no starting '::', no anonymous
++ }
++ if (const auto *TD = dyn_cast<TagDecl>(DC))
++ return createNestedNameSpecifier(Ctx, TD, FullyQualify, WithGlobalNsPrefix);
++ if (const auto *TDD = dyn_cast<TypedefNameDecl>(DC))
++ return createNestedNameSpecifier(Ctx, TDD, FullyQualify,
++ WithGlobalNsPrefix);
++ if (WithGlobalNsPrefix && DC->isTranslationUnit())
++ return NestedNameSpecifier::getGlobal();
++ return std::nullopt; // no starting '::' if |WithGlobalNsPrefix| is false
++}
++
++/// Return a fully qualified version of this name specifier.
++static inline NestedNameSpecifier getFullyQualifiedNestedNameSpecifier(
++ const ASTContext &Ctx, NestedNameSpecifier Scope,
++ bool WithGlobalNsPrefix) {
++ switch (Scope.getKind()) {
++ case NestedNameSpecifier::Kind::Null:
++ llvm_unreachable("can't fully qualify the empty nested name specifier");
++ case NestedNameSpecifier::Kind::Global:
++ case NestedNameSpecifier::Kind::MicrosoftSuper:
++ // Already fully qualified
++ return Scope;
++ case NestedNameSpecifier::Kind::Namespace:
++ return TypeName::createNestedNameSpecifier(
++ Ctx, Scope.getAsNamespaceAndPrefix().Namespace->getNamespace(),
++ WithGlobalNsPrefix);
++ case NestedNameSpecifier::Kind::Type: {
++ const Type *Type = Scope.getAsType();
++ // Find decl context.
++ const TypeDecl *TD;
++ if (const TagType *TagDeclType = Type->getAs<TagType>())
++ TD = TagDeclType->getDecl();
++ else if (const auto *D = dyn_cast<TypedefType>(Type))
++ TD = D->getDecl();
++ else
++ return Scope;
++ return TypeName::createNestedNameSpecifier(Ctx, TD, /*FullyQualify=*/true,
++ WithGlobalNsPrefix);
++ }
++ }
++ llvm_unreachable("bad NNS kind");
++}
++
++/// Create a nested name specifier for the declaring context of
++/// the type.
++static inline NestedNameSpecifier
++createNestedNameSpecifierForScopeOf(const ASTContext &Ctx, const Decl *Decl,
++ bool FullyQualified,
++ bool WithGlobalNsPrefix) {
++ assert(Decl);
++
++ // Some declaration cannot be qualified.
++ if (Decl->isTemplateParameter())
++ return std::nullopt;
++ const DeclContext *DC = Decl->getDeclContext()->getRedeclContext();
++ const auto *Outer = dyn_cast<NamedDecl>(DC);
++ const auto *OuterNS = dyn_cast<NamespaceDecl>(DC);
++ if (OuterNS && OuterNS->isAnonymousNamespace())
++ OuterNS = dyn_cast<NamespaceDecl>(OuterNS->getParent());
++ if (Outer) {
++#if 0
++ // QDoc divergence: upstream picks an arbitrary template specialization
++ // as the declaring context when a type is declared inside a class
++ // template but is not type-dependent. This produces unstable output
++ // (depends on specialization order) and is incorrect for QDoc's use
++ // case where we want the unspecialized template name.
++ // See QTBUG-144620.
++ if (const auto *CxxDecl = dyn_cast<CXXRecordDecl>(DC)) {
++ if (ClassTemplateDecl *ClassTempl =
++ CxxDecl->getDescribedClassTemplate()) {
++ if (!ClassTempl->specializations().empty()) {
++ Decl = *(ClassTempl->spec_begin());
++ Outer = dyn_cast<NamedDecl>(Decl);
++ OuterNS = dyn_cast<NamespaceDecl>(Decl);
++ }
++ }
++ }
++#endif
++
++ if (OuterNS) {
++ return createNestedNameSpecifier(Ctx, OuterNS, WithGlobalNsPrefix);
++ } else if (const auto *TD = dyn_cast<TagDecl>(Outer)) {
++ return createNestedNameSpecifier(
++ Ctx, TD, FullyQualified, WithGlobalNsPrefix);
++ } else if (isa<TranslationUnitDecl>(Outer)) {
++ // Context is the TU. Nothing needs to be done.
++ return std::nullopt;
++ } else {
++ // Decl's context was neither the TU, a namespace, nor a
++ // TagDecl, which means it is a type local to a scope, and not
++ // accessible at the end of the TU.
++ return std::nullopt;
++ }
++ } else if (WithGlobalNsPrefix && DC->isTranslationUnit()) {
++ return NestedNameSpecifier::getGlobal();
++ }
++ return std::nullopt;
++}
++
++/// Create a nested name specifier for the declaring context of
++/// the type.
++static inline NestedNameSpecifier
++createNestedNameSpecifierForScopeOf(const ASTContext &Ctx, const Type *TypePtr,
++ bool FullyQualified,
++ bool WithGlobalNsPrefix) {
++ if (!TypePtr)
++ return std::nullopt;
++
++ Decl *Decl = nullptr;
++ // There are probably other cases ...
++ if (const auto *TDT = dyn_cast<TypedefType>(TypePtr)) {
++ Decl = TDT->getDecl();
++ } else if (const auto *TagDeclType = dyn_cast<TagType>(TypePtr)) {
++ Decl = TagDeclType->getDecl();
++ } else if (const auto *TST = dyn_cast<TemplateSpecializationType>(TypePtr)) {
++ Decl = TST->getTemplateName().getAsTemplateDecl();
++ } else {
++ Decl = TypePtr->getAsCXXRecordDecl();
++ }
++
++ if (!Decl)
++ return std::nullopt;
++
++ return createNestedNameSpecifierForScopeOf(
++ Ctx, Decl, FullyQualified, WithGlobalNsPrefix);
++}
++
++inline NestedNameSpecifier
++createNestedNameSpecifier(const ASTContext &Ctx, const NamespaceDecl *Namespace,
++ bool WithGlobalNsPrefix) {
++ while (Namespace && Namespace->isInline()) {
++ // Ignore inline namespace;
++ Namespace = dyn_cast<NamespaceDecl>(Namespace->getDeclContext());
++ }
++ if (!Namespace)
++ return std::nullopt;
++
++ bool FullyQualify = true; // doesn't matter, DeclContexts are namespaces
++ return NestedNameSpecifier(
++ Ctx, Namespace,
++ createOuterNNS(Ctx, Namespace, FullyQualify, WithGlobalNsPrefix));
++}
++
++inline NestedNameSpecifier
++createNestedNameSpecifier(const ASTContext &Ctx, const TypeDecl *TD,
++ bool FullyQualify, bool WithGlobalNsPrefix) {
++ const Type *TypePtr = Ctx.getTypeDeclType(TD).getTypePtr();
++ if (auto *RD = dyn_cast<TagType>(TypePtr)) {
++ // We are asked to fully qualify and we have a Record Type (which
++ // may point to a template specialization) or Template
++ // Specialization Type. We need to fully qualify their arguments.
++ TypePtr = getFullyQualifiedTemplateType(
++ Ctx, RD, ElaboratedTypeKeyword::None,
++ createOuterNNS(Ctx, TD, FullyQualify, WithGlobalNsPrefix),
++ WithGlobalNsPrefix);
++ } else if (auto *TST = dyn_cast<TemplateSpecializationType>(TypePtr)) {
++ TypePtr = getFullyQualifiedTemplateType(Ctx, TST, WithGlobalNsPrefix);
++ }
++ return NestedNameSpecifier(TypePtr);
++}
++
++/// Return the fully qualified type, including fully-qualified
++/// versions of any template parameters.
++inline QualType getFullyQualifiedType(QualType QT, const ASTContext &Ctx,
++ bool WithGlobalNsPrefix = false) {
++ // In case of myType* we need to strip the pointer first, fully
++ // qualify and attach the pointer once again.
++ if (isa<PointerType>(QT.getTypePtr())) {
++ // Get the qualifiers.
++ Qualifiers Quals = QT.getQualifiers();
++ QT = getFullyQualifiedType(QT->getPointeeType(), Ctx, WithGlobalNsPrefix);
++ QT = Ctx.getPointerType(QT);
++ // Add back the qualifiers.
++ QT = Ctx.getQualifiedType(QT, Quals);
++ return QT;
++ }
++
++ if (auto *MPT = dyn_cast<MemberPointerType>(QT.getTypePtr())) {
++ // Get the qualifiers.
++ Qualifiers Quals = QT.getQualifiers();
++ // Fully qualify the pointee and class types.
++ QT = getFullyQualifiedType(QT->getPointeeType(), Ctx, WithGlobalNsPrefix);
++ NestedNameSpecifier Qualifier = getFullyQualifiedNestedNameSpecifier(
++ Ctx, MPT->getQualifier(), WithGlobalNsPrefix);
++ QT = Ctx.getMemberPointerType(QT, Qualifier,
++ MPT->getMostRecentCXXRecordDecl());
++ // Add back the qualifiers.
++ QT = Ctx.getQualifiedType(QT, Quals);
++ return QT;
++ }
++
++ // In case of myType& we need to strip the reference first, fully
++ // qualify and attach the reference once again.
++ if (isa<ReferenceType>(QT.getTypePtr())) {
++ // Get the qualifiers.
++ bool IsLValueRefTy = isa<LValueReferenceType>(QT.getTypePtr());
++ Qualifiers Quals = QT.getQualifiers();
++ QT = getFullyQualifiedType(QT->getPointeeType(), Ctx, WithGlobalNsPrefix);
++ // Add the r- or l-value reference type back to the fully
++ // qualified one.
++ if (IsLValueRefTy)
++ QT = Ctx.getLValueReferenceType(QT);
++ else
++ QT = Ctx.getRValueReferenceType(QT);
++ // Add back the qualifiers.
++ QT = Ctx.getQualifiedType(QT, Quals);
++ return QT;
++ }
++
++ // Handle types with attributes such as `unique_ptr<int> _Nonnull`.
++ if (auto *AT = dyn_cast<AttributedType>(QT.getTypePtr())) {
++ QualType NewModified =
++ getFullyQualifiedType(AT->getModifiedType(), Ctx, WithGlobalNsPrefix);
++ QualType NewEquivalent =
++ getFullyQualifiedType(AT->getEquivalentType(), Ctx, WithGlobalNsPrefix);
++ Qualifiers Qualifiers = QT.getLocalQualifiers();
++ return Ctx.getQualifiedType(
++ Ctx.getAttributedType(AT->getAttrKind(), NewModified, NewEquivalent),
++ Qualifiers);
++ }
++
++ // Remove the part of the type related to the type being a template
++ // parameter (we won't report it as part of the 'type name' and it
++ // is actually make the code below to be more complex (to handle
++ // those)
++ while (isa<SubstTemplateTypeParmType>(QT.getTypePtr())) {
++ // Get the qualifiers.
++ Qualifiers Quals = QT.getQualifiers();
++
++ QT = cast<SubstTemplateTypeParmType>(QT.getTypePtr())->desugar();
++
++ // Add back the qualifiers.
++ QT = Ctx.getQualifiedType(QT, Quals);
++ }
++
++ if (const auto *TST =
++ dyn_cast<const TemplateSpecializationType>(QT.getTypePtr())) {
++
++ const Type *T = getFullyQualifiedTemplateType(Ctx, TST, WithGlobalNsPrefix);
++ if (T == TST)
++ return QT;
++ return Ctx.getQualifiedType(T, QT.getQualifiers());
++ }
++
++ // Local qualifiers are attached to the QualType outside of the
++ // elaborated type. Retrieve them before descending into the
++ // elaborated type.
++ Qualifiers PrefixQualifiers = QT.getLocalQualifiers();
++ QT = QualType(QT.getTypePtr(), 0);
++
++ // We don't consider the alias introduced by `using a::X` as a new type.
++ // The qualified name is still a::X.
++ if (const auto *UT = QT->getAs<UsingType>()) {
++ QT = Ctx.getQualifiedType(UT->desugar(), PrefixQualifiers);
++ return getFullyQualifiedType(QT, Ctx, WithGlobalNsPrefix);
++ }
++
++ // Create a nested name specifier if needed.
++ NestedNameSpecifier Prefix = createNestedNameSpecifierForScopeOf(
++ Ctx, QT.getTypePtr(), true /*FullyQualified*/, WithGlobalNsPrefix);
++
++ // In case of template specializations iterate over the arguments and
++ // fully qualify them as well.
++ if (const auto *TT = dyn_cast<TagType>(QT.getTypePtr())) {
++ // We are asked to fully qualify and we have a Record Type (which
++ // may point to a template specialization) or Template
++ // Specialization Type. We need to fully qualify their arguments.
++
++ const Type *TypePtr = getFullyQualifiedTemplateType(
++ Ctx, TT, TT->getKeyword(), Prefix, WithGlobalNsPrefix);
++ QT = QualType(TypePtr, 0);
++ } else if (const auto *TT = dyn_cast<TypedefType>(QT.getTypePtr())) {
++ // QDoc divergence: prefer the existing qualifier from the TypedefType
++ // when available, falling back to the computed Prefix. This preserves
++ // member type alias qualifiers (e.g., QList<QVariant>::parameter_type)
++ // that would otherwise be lost when the Prefix is recomputed from the
++ // declaring context. See QTBUG-144620.
++ NestedNameSpecifier TypedefPrefix = TT->getQualifier();
++ QT = Ctx.getTypedefType(
++ TT->getKeyword(), TypedefPrefix ? TypedefPrefix : Prefix,
++ TT->getDecl(),
++ getFullyQualifiedType(TT->desugar(), Ctx, WithGlobalNsPrefix));
++ } else {
++ // QDoc divergence: upstream asserts here (!Prefix && "Unhandled type node").
++ // QDoc encounters types (such as AutoType and BuiltinType) that may have
++ // a non-null Prefix but are not TagType or TypedefType. Silently dropping
++ // the prefix is safe — it only affects qualification of the printed name.
++ }
++ QT = Ctx.getQualifiedType(QT, PrefixQualifiers);
++ return QT;
++}
++
++#else // CLANG_VERSION_MAJOR < 22
++
++// =========================================================================
++// Pre-LLVM 22 implementation
++//
++// This block is the existing fork, unchanged. It supports LLVM 17–21
++// with version-specific guards for API differences between those releases.
++// =========================================================================
++
+ inline QualType getFullyQualifiedType(QualType QT, const ASTContext &Ctx,
+ bool WithGlobalNsPrefix);
+
+@@ -503,6 +991,8 @@ inline QualType getFullyQualifiedType(QualType QT, const ASTContext &Ctx,
+ return QT;
+ }
+
++#endif // CLANG_VERSION_MAJOR >= 22
++
+ inline std::string getFullyQualifiedName(QualType QT,
+ const ASTContext &Ctx,
+ const PrintingPolicy &Policy,
+--
+cgit v1.2.3
+