[clang] [Sema] Reject unqualified lookup of local nested class name (PR #192678)
via cfe-commits
cfe-commits at lists.llvm.org
Thu May 14 22:55:43 PDT 2026
https://github.com/arrowten updated https://github.com/llvm/llvm-project/pull/192678
>From 014d410f76ad658139bef8ac8a0d1771652d256e Mon Sep 17 00:00:00 2001
From: Ajay Wakodikar <ajaywakodikarsocial at gmail.com>
Date: Fri, 8 May 2026 07:49:43 -0400
Subject: [PATCH] [Sema] Reject unqualified lookup of local nested class
---
clang/docs/ReleaseNotes.rst | 1 +
clang/lib/Sema/SemaDecl.cpp | 24 +++++-
.../Sema/unqualified-lookup-local-class.cpp | 77 +++++++++++++++++++
3 files changed, 100 insertions(+), 2 deletions(-)
create mode 100644 clang/test/Sema/unqualified-lookup-local-class.cpp
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 745f9e6556eec..c54b6e5a1757e 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -614,6 +614,7 @@ Bug Fixes to C++ Support
- Fixed an invalid rejection and assertion failure while generating ``operator=`` for fields with the ``__restrict`` qualifier. (#GH37979)
- Fixed a use-after-free bug when parsing default arguments containing lambdas in declarations with template-id declarators. (#GH196725)
- Fixed a crash in constant evaluation using placement new on an array which was later initialized. (#GH196450)
+- Fixed an issue where Clang incorrectly accepted invalid unqualified uses of local nested class names outside their declaring scope. (#GH184622)
Bug Fixes to AST Handling
^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp
index bec351528760a..4b9576479e29e 100644
--- a/clang/lib/Sema/SemaDecl.cpp
+++ b/clang/lib/Sema/SemaDecl.cpp
@@ -1585,8 +1585,28 @@ void Sema::PushOnScopeChains(NamedDecl *D, Scope *S, bool AddToContext) {
// Out-of-line definitions shouldn't be pushed into scope in C++, unless they
// are function-local declarations.
- if (getLangOpts().CPlusPlus && D->isOutOfLine() && !S->getFnParent())
- return;
+ if (getLangOpts().CPlusPlus && D->isOutOfLine()) {
+ if (!S->getFnParent())
+ return;
+
+ // Even inside a function, an out-of-line definition of a type that is
+ // nested inside a local class must not be pushed into the enclosing
+ // function scope. For example:
+ //
+ // class A { public: class B; };
+ // class A::B {}; // out-of-line definition inside the function
+ // B b; // must fail - only A::B is valid
+ // Wrapper{B{}} // must also fail
+ //
+ // Per C++ scoping rules only the qualified form A::B is accessible.
+ // Without this guard, PushOnScopeChains would add B to the function's
+ // local scope, making it findable via unqualified lookup, which is
+ // incorrect. The condition targets TagDecls (class/struct/union/enum)
+ // whose DeclContext is a CXXRecordDecl, i.e., types that are members
+ // of a local class being defined out-of-line.
+ if (isa<TagDecl>(D) && isa<CXXRecordDecl>(D->getDeclContext()))
+ return;
+ }
// Template instantiations should also not be pushed into scope.
if (isa<FunctionDecl>(D) &&
diff --git a/clang/test/Sema/unqualified-lookup-local-class.cpp b/clang/test/Sema/unqualified-lookup-local-class.cpp
new file mode 100644
index 0000000000000..bdc6d0b0e38ee
--- /dev/null
+++ b/clang/test/Sema/unqualified-lookup-local-class.cpp
@@ -0,0 +1,77 @@
+// RUN: %clang_cc1 -std=c++20 -verify %s
+
+// Regression test for PR184622: out-of-line definitions of nested classes
+// inside a function must not be visible via unqualified lookup.
+
+// Unqualified member access inside a class body is fine.
+struct A1 {
+ int x;
+ void f() { x = 1; } // OK
+};
+
+struct Base {
+ int y;
+};
+
+struct Derived : Base {
+ void f() { y = 2; } // OK - inherited member
+};
+
+struct A2 { int z; };
+struct B2 : A2 {
+ using A2::z;
+ void f() { z = 3; } // OK
+};
+
+// Out-of-line definitions at namespace scope are fine.
+class OuterNS { public: class Inner; };
+class OuterNS::Inner {}; // OK - not inside a function
+
+// Out-of-line definitions of types nested in a class inside a function:
+// the qualified form must work, but the unqualified name must not leak.
+void pass1() {
+ struct A { struct B {}; };
+ A::B b; // OK - qualified
+
+ class C { public: class B; };
+ class C::B {};
+ C::B b2; // OK - qualified
+}
+
+template<class T>
+struct Wrapper { Wrapper(T) {} };
+
+// class nested in class, defined out-of-line inside function - unqualified 'B'
+// must not be found.
+void fail_class() {
+ class A { public: class B; };
+ class A::B {};
+ B b; // expected-error {{unknown type name 'B'}}
+ Wrapper w = Wrapper{B{}}; // expected-error {{use of undeclared identifier 'B'}}
+ A::B ok; // OK - qualified lookup still works
+}
+
+// Same rule applies to structs (TagDecl covers class/struct/union/enum).
+void fail_struct() {
+ struct Outer { struct Inner; };
+ struct Outer::Inner {};
+ Inner bad; // expected-error {{unknown type name 'Inner'}}
+ Outer::Inner good; // OK
+}
+
+// Union nested in a class inside a function.
+void fail_union() {
+ struct S { union U; };
+ union S::U { int a; float b; }; // expected-note {{'S::U' declared here}}
+ U bad; // expected-error {{unknown type name 'U'}}
+ S::U ok; // OK
+}
+
+// Multiple independent functions: each has its own scope, so 'B' in fail2
+// is unrelated to 'B' in fail_class above.
+void fail2() {
+ class Outer { public: class Inner; };
+ class Outer::Inner {};
+ Inner bad; // expected-error {{unknown type name 'Inner'}}
+ Outer::Inner good; // OK
+}
More information about the cfe-commits
mailing list