[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