[clang] [Clang][Sema] Keep intermediate dependent members dependent in implicit-typename qualifiers (PR #205762)
via cfe-commits
cfe-commits at lists.llvm.org
Thu Jun 25 02:55:01 PDT 2026
https://github.com/likeamahoney created https://github.com/llvm/llvm-project/pull/205762
Fixes [174301](https://github.com/llvm/llvm-project/issues/174301)
When a dependent qualified type is written without the `typename` keyword (the C++20 / extension "implicit typename" recovery), its nested-name-specifier is parsed while "entering" a context. In a class or at namespace scope the parser cannot yet tell a type-specifier apart from an out-of-line member definition, so it parses the prefix in entering mode. As a side effect an intermediate dependent member -- e.g. `B` in `A<T>::B::C` - is speculatively resolved against the template pattern instead of being left as a dependent name.
Such a resolved nested-name-specifier does not survive instantiation: the member is never re-resolved against the instantiated parent, so the qualified type stays spuriously dependent in otherwise non-dependent code. That dependent type then makes CTAD / `auto` deduction silently bail out, leaving an undeduced placeholder that crashes codegen with:
Assertion `!A->getDeducedType().isNull() && "cannot request the size of an undeduced or dependent auto type"' failed.
Once `getTypeName` knows the qualified-id names a type (the implicit-typename path), rebuild the qualifier so that every component resolved against a dependent (non current-instantiation) context becomes a `DependentNameType` - exactly the type produced when `typename` is written explicitly. This is the recovery the existing FIXME in `ActOnTemplateIdType` refers to. Out-of-line definitions are parsed as declarators and do not reach this path, so they are unaffected.
>From 81b67925efc2ed6b60220e5aa841343a75d8af68 Mon Sep 17 00:00:00 2001
From: likeamahoney <likeamahoney at gmail.com>
Date: Thu, 25 Jun 2026 12:41:16 +0300
Subject: [PATCH] [Clang][Sema] Keep intermediate dependent members dependent
in implicit-typename qualifiers
When a dependent qualified type is written without the `typename` keyword
(the C++20 / extension "implicit typename" recovery), its nested-name-specifier
is parsed while "entering" a context. Whenever the declaration sits in a class or
at namespace scope the parser cannot yet tell a type-specifier apart from an
out-of-line member definition, so it parses the prefix in entering mode. As a
side effect an intermediate dependent member -- e.g. `B` in `A<T>::B::C` -- is
speculatively resolved against the template pattern instead of being left as a
dependent name.
Such a resolved nested-name-specifier does not survive instantiation: the member
is never re-resolved against the instantiated parent, so the qualified type stays
spuriously dependent in otherwise non-dependent code. That dependent type then
makes CTAD / `auto` deduction silently bail out, leaving an undeduced placeholder
that crashes codegen:
Assertion `!A->getDeducedType().isNull() && "cannot request the size of an
undeduced or dependent auto type"' failed.
Once `getTypeName` knows the qualified-id names a type (the implicit-typename
path), rebuild the qualifier so that every component resolved against a dependent
(non current-instantiation) context becomes a `DependentNameType` -- exactly the
type produced when `typename` is written explicitly. This is the recovery the
existing FIXME in `ActOnTemplateIdType` refers to. Out-of-line definitions are
parsed as declarators and do not reach this path, so they are unaffected.
Fixes #174301
---
clang/docs/ReleaseNotes.rst | 4 +
clang/lib/Sema/SemaDecl.cpp | 91 +++++++++++++++++++
.../implicit-typename-instantiation.cpp | 52 +++++++++++
3 files changed, 147 insertions(+)
create mode 100644 clang/test/SemaTemplate/implicit-typename-instantiation.cpp
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index d4c286644033b..a0764ae606492 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -812,6 +812,10 @@ Bug Fixes to C++ Support
- Fixed an issue where Clang incorrectly accepted invalid unqualified uses of local nested class names outside their declaring scope. (#GH184622)
- Fixed a crash when parsing invalid friend declaration with storage-class specifier. (#GH186569)
- Fixed a missing vtable for ``dynamic_cast<FinalClass *>(this)`` in a function template. (#GH198511)
+- Fixed a crash when a dependent qualified type whose nested-name-specifier contains an
+ intermediate dependent member (e.g. ``A<T>::B::C``) was written without the ``typename``
+ keyword. The intermediate member was speculatively resolved against the template pattern and
+ the resulting type spuriously stayed dependent after instantiation. (#GH174301)
Bug Fixes to AST Handling
^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp
index d45c3eb35094f..71f9c9f016f8b 100644
--- a/clang/lib/Sema/SemaDecl.cpp
+++ b/clang/lib/Sema/SemaDecl.cpp
@@ -273,6 +273,82 @@ static ParsedType recoverFromTypeInKnownDependentBase(Sema &S,
return S.CreateParsedType(T, Builder.getTypeSourceInfo(Context, T));
}
+/// A dependent qualified type written without the 'typename' keyword (accepted
+/// as the C++20 / extension "implicit typename" recovery) has its
+/// nested-name-specifier parsed while "entering" a context. As a side effect,
+/// intermediate dependent members -- e.g. 'B' in 'A<T>::B::C' -- are
+/// speculatively resolved against the template pattern instead of being left as
+/// dependent names. Such a resolved nested-name-specifier does not survive
+/// template instantiation: the member is never re-resolved against the
+/// instantiated parent, so a spuriously dependent type leaks into otherwise
+/// non-dependent code (and later crashes e.g. CTAD / codegen).
+///
+/// Rebuild the qualifier so that every component resolved against a dependent
+/// (non current-instantiation) context becomes a DependentNameType, matching
+/// the type produced when 'typename' is written explicitly.
+static NestedNameSpecifierLoc
+rebuildDependentlyResolvedQualifier(Sema &S, NestedNameSpecifierLoc QLoc) {
+ if (!QLoc || QLoc.getNestedNameSpecifier().getKind() !=
+ NestedNameSpecifier::Kind::Type)
+ return QLoc;
+
+ TypeLoc TL = QLoc.castAsTypeLoc();
+
+ // Only members that were resolved to a concrete declaration (typedef, tag, or
+ // using) carry a qualifier + name we could re-express as a dependent name.
+ NestedNameSpecifierLoc PrefixLoc;
+ SourceLocation KeywordLoc, NameLoc;
+ const IdentifierInfo *II = nullptr;
+ if (auto T = TL.getAs<TypedefTypeLoc>()) {
+ PrefixLoc = T.getQualifierLoc();
+ KeywordLoc = T.getElaboratedKeywordLoc();
+ NameLoc = T.getNameLoc();
+ II = T.getDecl()->getIdentifier();
+ } else if (auto T = TL.getAs<TagTypeLoc>()) {
+ PrefixLoc = T.getQualifierLoc();
+ KeywordLoc = T.getElaboratedKeywordLoc();
+ NameLoc = T.getNameLoc();
+ II = T.getDecl()->getIdentifier();
+ } else if (auto T = TL.getAs<UsingTypeLoc>()) {
+ PrefixLoc = T.getQualifierLoc();
+ KeywordLoc = T.getElaboratedKeywordLoc();
+ NameLoc = T.getNameLoc();
+ II = T.getDecl()->getIdentifier();
+ } else if (auto T = TL.getAs<UnresolvedUsingTypeLoc>()) {
+ PrefixLoc = T.getQualifierLoc();
+ KeywordLoc = T.getElaboratedKeywordLoc();
+ NameLoc = T.getNameLoc();
+ II = T.getDecl()->getIdentifier();
+ } else {
+ return QLoc;
+ }
+
+ if (!II || !PrefixLoc)
+ return QLoc;
+
+ NestedNameSpecifierLoc NewPrefixLoc =
+ rebuildDependentlyResolvedQualifier(S, PrefixLoc);
+
+ // The member can only be left unresolved (a dependent name) if its prefix is
+ // itself dependent; otherwise the original resolution is correct.
+ if (!NewPrefixLoc.getNestedNameSpecifier().isDependent())
+ return QLoc;
+
+ ASTContext &Ctx = S.Context;
+ QualType DNT = Ctx.getDependentNameType(
+ ElaboratedTypeKeyword::None, NewPrefixLoc.getNestedNameSpecifier(), II);
+
+ TypeLocBuilder TLB;
+ auto DTL = TLB.push<DependentNameTypeLoc>(DNT);
+ DTL.setElaboratedKeywordLoc(KeywordLoc);
+ DTL.setQualifierLoc(NewPrefixLoc);
+ DTL.setNameLoc(NameLoc);
+
+ CXXScopeSpec Rebuilt;
+ Rebuilt.Make(Ctx, TLB.getTypeLocInContext(Ctx, DNT), QLoc.getEndLoc());
+ return Rebuilt.getWithLocInContext(Ctx);
+}
+
ParsedType Sema::getTypeName(const IdentifierInfo &II, SourceLocation NameLoc,
Scope *S, CXXScopeSpec *SS, bool isClassName,
bool HasTrailingDot, ParsedType ObjectTypePtr,
@@ -298,6 +374,21 @@ ParsedType Sema::getTypeName(const IdentifierInfo &II, SourceLocation NameLoc,
if (!LookupCtx) {
if (isDependentScopeSpecifier(*SS)) {
+ // The qualifier is dependent and we now know (from the grammar) that
+ // this names a type. If it was parsed while entering a context, an
+ // intermediate dependent member may have been speculatively resolved
+ // against the template pattern; rebuild it as a dependent name so it
+ // survives instantiation. See rebuildDependentlyResolvedQualifier.
+ if (NestedNameSpecifierLoc QLoc = SS->getWithLocInContext(Context)) {
+ NestedNameSpecifierLoc NewQLoc =
+ rebuildDependentlyResolvedQualifier(*this, QLoc);
+ if (NewQLoc.getNestedNameSpecifier() !=
+ QLoc.getNestedNameSpecifier()) {
+ SS->clear();
+ SS->Adopt(NewQLoc);
+ }
+ }
+
// C++ [temp.res]p3:
// A qualified-id that refers to a type and in which the
// nested-name-specifier depends on a template-parameter (14.6.2)
diff --git a/clang/test/SemaTemplate/implicit-typename-instantiation.cpp b/clang/test/SemaTemplate/implicit-typename-instantiation.cpp
new file mode 100644
index 0000000000000..22c8e23b2d92c
--- /dev/null
+++ b/clang/test/SemaTemplate/implicit-typename-instantiation.cpp
@@ -0,0 +1,52 @@
+// RUN: %clang_cc1 -fsyntax-only -verify=cxx17 -std=c++17 %s
+// RUN: %clang_cc1 -fsyntax-only -verify=cxx20 -std=c++20 %s
+// cxx20-no-diagnostics
+
+// When 'typename' is omitted from a dependent qualified type whose
+// nested-name-specifier contains an intermediate dependent member
+// (e.g. the 'Inner' / 'Node_traits' below), the implicit-typename recovery
+// used to leave the intermediate member speculatively resolved against the
+// template pattern. That nested-name-specifier did not survive instantiation,
+// producing a spuriously dependent type that crashed CTAD and codegen.
+// Omitting and writing 'typename' must produce the same type.
+
+namespace intermediate_typedef {
+template <class T> struct Iter { T *p; };
+template <class T> struct Traits { typedef Iter<T> Const_iterator; };
+template <class T> struct ListBase { typedef Traits<T> Node_traits; };
+
+template <class T> struct List {
+ // cxx17-warning at +1 {{missing 'typename' prior to dependent type name 'ListBase<T>::Node_traits::Const_iterator' is a C++20 extension}}
+ typedef ListBase<T>::Node_traits::Const_iterator const_iterator;
+ const_iterator begin() { return const_iterator(); }
+};
+
+struct S {};
+template <class U> void use(U) {}
+
+void test() {
+ List<S> lst;
+ Iter it = lst.begin(); // CTAD; previously crashed.
+ use(it);
+ static_assert(__is_same(decltype(it), Iter<S>));
+}
+} // namespace intermediate_typedef
+
+namespace intermediate_tag {
+// Reduced from llvm/llvm-project#174301.
+template <class T> struct Tester {
+ struct Inner { using T2 = int; };
+ static void test();
+};
+
+template <class T>
+// cxx17-warning at +1 {{missing 'typename' prior to dependent type name 'Tester<T>::Inner::T2' is a C++20 extension}}
+Tester<T>::Inner::T2 getInnerT2() { return {}; }
+
+template <class T> void Tester<T>::test() {
+ auto x = getInnerT2<T>();
+ static_assert(__is_same(decltype(x), int));
+}
+
+template struct Tester<int>;
+} // namespace intermediate_tag
More information about the cfe-commits
mailing list