From f75f4090ed8e5c9dc898a313a5dd9b6878c2f034 Mon Sep 17 00:00:00 2001 From: Paul Wicking 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 --- 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(TD); + if (!TSTDecl) + return Ctx.getTagType(Keyword, Qualifier, TD, /*OwnsTag=*/false) + .getTypePtr(); + + const TemplateArgumentList &TemplateArgs = TSTDecl->getTemplateArgs(); + + bool MightHaveChanged = false; + SmallVector 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 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(DC)) { + while (NS && NS->isInline()) { + // Ignore inline namespace; + NS = dyn_cast(NS->getDeclContext()); + } + if (NS && NS->getDeclName()) { + return createNestedNameSpecifier(Ctx, NS, WithGlobalNsPrefix); + } + return std::nullopt; // no starting '::', no anonymous + } + if (const auto *TD = dyn_cast(DC)) + return createNestedNameSpecifier(Ctx, TD, FullyQualify, WithGlobalNsPrefix); + if (const auto *TDD = dyn_cast(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()) + TD = TagDeclType->getDecl(); + else if (const auto *D = dyn_cast(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(DC); + const auto *OuterNS = dyn_cast(DC); + if (OuterNS && OuterNS->isAnonymousNamespace()) + OuterNS = dyn_cast(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(DC)) { + if (ClassTemplateDecl *ClassTempl = + CxxDecl->getDescribedClassTemplate()) { + if (!ClassTempl->specializations().empty()) { + Decl = *(ClassTempl->spec_begin()); + Outer = dyn_cast(Decl); + OuterNS = dyn_cast(Decl); + } + } + } +#endif + + if (OuterNS) { + return createNestedNameSpecifier(Ctx, OuterNS, WithGlobalNsPrefix); + } else if (const auto *TD = dyn_cast(Outer)) { + return createNestedNameSpecifier( + Ctx, TD, FullyQualified, WithGlobalNsPrefix); + } else if (isa(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(TypePtr)) { + Decl = TDT->getDecl(); + } else if (const auto *TagDeclType = dyn_cast(TypePtr)) { + Decl = TagDeclType->getDecl(); + } else if (const auto *TST = dyn_cast(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(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(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(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(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(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(QT.getTypePtr())) { + // Get the qualifiers. + bool IsLValueRefTy = isa(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 _Nonnull`. + if (auto *AT = dyn_cast(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(QT.getTypePtr())) { + // Get the qualifiers. + Qualifiers Quals = QT.getQualifiers(); + + QT = cast(QT.getTypePtr())->desugar(); + + // Add back the qualifiers. + QT = Ctx.getQualifiedType(QT, Quals); + } + + if (const auto *TST = + dyn_cast(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()) { + 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(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(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::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