[clang] 9a9daa9 - [Clang] Keep scope annotations covering typo-corrected qualifiers (#223978)

via cfe-commits cfe-commits at lists.llvm.org
Sat Oct 3 08:27:07 PDT 2026


Author: Akash Manna
Date: 2026-10-03T15:26:53Z
New Revision: 9a9daa97acbd8a5e31daea241bb5e5ee0a5de171

URL: https://github.com/llvm/llvm-project/commit/9a9daa97acbd8a5e31daea241bb5e5ee0a5de171
DIFF: https://github.com/llvm/llvm-project/commit/9a9daa97acbd8a5e31daea241bb5e5ee0a5de171.diff

LOG: [Clang] Keep scope annotations covering typo-corrected qualifiers (#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.

Added: 
    

Modified: 
    clang/docs/ReleaseNotes.md
    clang/lib/Parse/ParseExprCXX.cpp
    clang/test/Parser/cxx-ambig-paren-expr.cpp

Removed: 
    


################################################################################
diff  --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index c99b871cc1e20..1c041d4bf54e1 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -663,6 +663,10 @@ features cannot lower the translation-unit ABI level;
 - Fixed a bug where top-level CV qualifiers (such as ``const``) were dropped from pointers modified by Microsoft pointer attributes (like ``__ptr32`` and ``__ptr64``) and WebAssembly's ``__funcref``.
 
 - Fixed a bug where we accepted ``__super`` being qualified by a scope specifier, causing codegen to assertion fail elsewhere. (#GH212988)
+- 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)
 - Fixed an issue where we tried to compare invalid NTTPs for variable declarations, which ended up in hitting an assertion with a constrained non-plain-auto NTTP, which we don't quite implement yet. (#GH208658)
 
 - Fixed a crash when a using-declaration naming an unresolvable member of a

diff  --git a/clang/lib/Parse/ParseExprCXX.cpp b/clang/lib/Parse/ParseExprCXX.cpp
index c3ec0f03abd62..bfdad0d501282 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();
 
@@ -365,20 +376,14 @@ bool Parser::ParseOptionalCXXScopeSpecifier(
                                          TemplateId->NumArgs);
 
       if (TemplateId->isInvalid() ||
-          Actions.ActOnCXXNestedNameSpecifier(getCurScope(),
-                                              SS,
-                                              TemplateId->TemplateKWLoc,
-                                              TemplateId->Template,
-                                              TemplateId->TemplateNameLoc,
-                                              TemplateId->LAngleLoc,
-                                              TemplateArgsPtr,
-                                              TemplateId->RAngleLoc,
-                                              CCLoc,
-                                              EnteringContext)) {
-        SourceLocation StartLoc
-          = SS.getBeginLoc().isValid()? SS.getBeginLoc()
-                                      : TemplateId->TemplateNameLoc;
+          Actions.ActOnCXXNestedNameSpecifier(
+              getCurScope(), SS, TemplateId->TemplateKWLoc,
+              TemplateId->Template, TemplateId->TemplateNameLoc,
+              TemplateId->LAngleLoc, TemplateArgsPtr, TemplateId->RAngleLoc,
+              CCLoc, EnteringContext)) {
         SS.SetInvalid(SourceRange(StartLoc, CCLoc));
+      } else {
+        RestoreScopeSpecRange(SourceRange(StartLoc, CCLoc));
       }
 
       continue;
@@ -473,6 +478,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;
@@ -489,11 +495,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:
@@ -517,6 +526,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,
@@ -528,6 +540,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'?}}
+}
+


        


More information about the cfe-commits mailing list