https://github.com/akash-manna-sky created 
https://github.com/llvm/llvm-project/pull/223978

Fixes #221890

While tentatively deciding whether `(void(foo::S<int>))` is a type-id, the 
parser annotates `foo::S<int>`. Lookup of `S` in `foo` fails, typo correction 
suggests plain `S`, and Sema rebuilds the `CXXScopeSpec` without the qualifier. 
The annotation token takes its start from that scope specifier, so it only 
covers `S<int>` and the already-consumed `foo ::` tokens are never replaced in 
the token cache. They resurface after the tentative parse backtracks, every 
later parse sees `foo :: <type>`, and the second `isTypeIdInParens()` in 
`ParseCXXAmbiguousParenExpression` contradicts the first.

The parser owns the invariant that an annotation replaces every token consumed 
to produce it, so the fix lives in `ParseOptionalCXXScopeSpecifier`. After each 
Sema call that can rewrite the qualifier through typo correction, the scope 
specifier is re-ranged over the tokens actually consumed. When the qualifier 
was dropped entirely, the template-id annotation is extended back over those 
tokens instead. Sema and the typo-correction recovery are untouched.

>From 9ad28c96f8fde0a66112bc9273377b5ffc81b0af Mon Sep 17 00:00:00 2001
From: Akash Manna <[email protected]>
Date: Wed, 16 Sep 2026 16:29:10 +0530
Subject: [PATCH] [Clang] Keep scope annotations covering typo-corrected
 qualifiers

When typo correction replaces or drops a qualifier during
nested-name-specifier annotation, Sema hands back a CXXScopeSpec whose
range no longer starts at the tokens the parser already consumed. The
annotation token built from it then fails to replace those tokens in
the preprocessor's cache, and they resurface after a tentative parse
backtracks, e.g. as `foo :: annot_typename`. In
ParseCXXAmbiguousParenExpression this makes the second
isTypeIdInParens() disagree with the first and trips its assertion.

Restore the scope specifier's range over the consumed tokens after
each Sema call that can rewrite it, and when the qualifier was dropped
entirely, extend the template-id annotation over those tokens instead.

Fixes #221890
---
 clang/docs/ReleaseNotes.md                 |  5 ++++
 clang/lib/Parse/ParseExprCXX.cpp           | 29 +++++++++++++++++++---
 clang/test/Parser/cxx-ambig-paren-expr.cpp | 29 ++++++++++++++++++++++
 3 files changed, 60 insertions(+), 3 deletions(-)

diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index 3cca316a91d4d..ac39f24fd3f68 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -676,6 +676,11 @@ features cannot lower the translation-unit ABI level;
   class with an invalid non-static data member, such as one qualified with an
   address space. (#GH194605)
 
+- Fixed an assertion when typo correction replaced or dropped the qualifier of
+  a name such as `foo::S<int>` while the parser was deciding whether a
+  parenthesized construct like `(void(foo::S<int>))` is a type-id or an
+  expression. (#GH221890)
+
 #### Bug Fixes to AST Handling
 
 - Fixed a non-deterministic ordering of unused local typedefs that made
diff --git a/clang/lib/Parse/ParseExprCXX.cpp b/clang/lib/Parse/ParseExprCXX.cpp
index 860c069e18fca..ab92837f8761a 100644
--- a/clang/lib/Parse/ParseExprCXX.cpp
+++ b/clang/lib/Parse/ParseExprCXX.cpp
@@ -238,6 +238,14 @@ bool Parser::ParseOptionalCXXScopeSpecifier(
     }
   }
 
+  // Typo correction may replace a qualifier we have already consumed the 
tokens
+  // for. The scope specifier must still cover those tokens, or the annotation
+  // built from it won't replace them and they reappear after backtracking.
+  auto RestoreScopeSpecRange = [&](SourceRange Range) {
+    if (Range.isValid() && SS.isValid() && SS.getRange() != Range)
+      SS.MakeTrivial(Actions.getASTContext(), SS.getScopeRep(), Range);
+  };
+
   // Preferred type might change when parsing qualifiers, we need the original.
   auto SavedType = PreferredType;
   while (true) {
@@ -353,6 +361,9 @@ bool Parser::ParseOptionalCXXScopeSpecifier(
       if (LastII)
         *LastII = TemplateId->Name;
 
+      SourceLocation StartLoc =
+          SS.getBeginLoc().isValid() ? SS.getBeginLoc() : Tok.getLocation();
+
       // Consume the template-id token.
       ConsumeAnnotationToken();
 
@@ -375,10 +386,9 @@ bool Parser::ParseOptionalCXXScopeSpecifier(
                                               TemplateId->RAngleLoc,
                                               CCLoc,
                                               EnteringContext)) {
-        SourceLocation StartLoc
-          = SS.getBeginLoc().isValid()? SS.getBeginLoc()
-                                      : TemplateId->TemplateNameLoc;
         SS.SetInvalid(SourceRange(StartLoc, CCLoc));
+      } else {
+        RestoreScopeSpecRange(SourceRange(StartLoc, CCLoc));
       }
 
       continue;
@@ -472,6 +482,7 @@ bool Parser::ParseOptionalCXXScopeSpecifier(
              "NextToken() not working properly!");
       Token ColonColon = Tok;
       SourceLocation CCLoc = ConsumeToken();
+      SourceLocation ScopeBeginLoc = SS.getBeginLoc();
 
       bool IsCorrectedToColon = false;
       bool *CorrectionFlagPtr = ColonIsSacred ? &IsCorrectedToColon : nullptr;
@@ -488,11 +499,14 @@ bool Parser::ParseOptionalCXXScopeSpecifier(
           break;
         }
         SS.SetInvalid(SourceRange(IdLoc, CCLoc));
+      } else {
+        RestoreScopeSpecRange(SourceRange(ScopeBeginLoc, CCLoc));
       }
       HasScopeSpecifier = true;
       continue;
     }
 
+    SourceRange ScopeRange = SS.getRange();
     CheckForTemplateAndDigraph(Next, ObjectType, EnteringContext, II, SS);
 
     // nested-name-specifier:
@@ -516,6 +530,9 @@ bool Parser::ParseOptionalCXXScopeSpecifier(
             isTemplateArgumentList(1) == TPResult::False)
           break;
 
+        RestoreScopeSpecRange(ScopeRange);
+        bool DroppedScope = ScopeRange.isValid() && SS.isEmpty();
+
         // We have found a template name, so annotate this token
         // with a template-id annotation. We do not permit the
         // template-id to be translated into a type annotation,
@@ -527,6 +544,12 @@ bool Parser::ParseOptionalCXXScopeSpecifier(
         if (AnnotateTemplateIdToken(Template, TNK, SS, SourceLocation(),
                                     TemplateName, false))
           return true;
+        if (DroppedScope) {
+          // No scope specifier is left to cover the dropped qualifier's
+          // tokens, so extend the template-id annotation over them.
+          Tok.setLocation(ScopeRange.getBegin());
+          PP.AnnotateCachedTokens(Tok);
+        }
         continue;
       }
 
diff --git a/clang/test/Parser/cxx-ambig-paren-expr.cpp 
b/clang/test/Parser/cxx-ambig-paren-expr.cpp
index cc509f7b059f3..160ce4c3bb72f 100644
--- a/clang/test/Parser/cxx-ambig-paren-expr.cpp
+++ b/clang/test/Parser/cxx-ambig-paren-expr.cpp
@@ -70,3 +70,32 @@ void test(int i) {
     return;
 }
 
+namespace GH221890 {
+template <class T> struct S {}; // expected-note 2 {{'S' declared here}}
+struct Plain {}; // expected-note {{'Plain' declared here}}
+namespace foo {}
+namespace ns1 { template <class T> struct Q {}; } // expected-note {{'ns1::Q' 
declared here}}
+namespace ba { template <class T> struct T2 {}; struct P {}; } // 
expected-note 2 {{'ba' declared here}}
+namespace bar {}
+
+// Typo correction dropped or replaced the qualifier while the parser was
+// tentatively deciding whether the parenthesized construct is a type-id, and
+// the tokens of the original qualifier resurfaced after backtracking.
+int a = (void(foo::S<int>)); // expected-error {{no template named 'S' in 
namespace 'GH221890::foo'; did you mean simply 'S'?}} \
+                             // expected-error {{expected '(' for 
function-style cast or type construction}}
+int b = (void(foo::Q<int>)); // expected-error {{no template named 'Q' in 
namespace 'GH221890::foo'; did you mean 'ns1::Q'?}} \
+                             // expected-error {{expected '(' for 
function-style cast or type construction}}
+int c = (void(bar::ba::T2<int>)); // expected-error {{no member named 'ba' in 
namespace 'GH221890::bar'; did you mean simply 'ba'?}} \
+                                  // expected-error {{expected '(' for 
function-style cast or type construction}}
+int d = (void(bar::ba::P)); // expected-error {{no member named 'ba' in 
namespace 'GH221890::bar'; did you mean simply 'ba'?}} \
+                            // expected-error {{expected '(' for 
function-style cast or type construction}}
+void f() {
+  void(foo::S<int>); // expected-error {{no template named 'S' in namespace 
'GH221890::foo'; did you mean simply 'S'?}} \
+                     // expected-error {{expected '(' for function-style cast 
or type construction}}
+}
+
+// The same constructs without a typo, and with a non-template name.
+int e = (void(S<int>)); // expected-error {{expected '(' for function-style 
cast or type construction}}
+int g = (void(foo::Plain)); // expected-error {{no member named 'Plain' in 
namespace 'GH221890::foo'; did you mean simply 'Plain'?}}
+}
+

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to