[clang] [clang][Parser] Improve error recovery for missing semicolons in class members. (PR #190744)
Haojian Wu via cfe-commits
cfe-commits at lists.llvm.org
Thu Apr 16 00:05:17 PDT 2026
https://github.com/hokein updated https://github.com/llvm/llvm-project/pull/190744
>From e241e09a02ad1a9e795e44aa86ddcfaaa17a2bb6 Mon Sep 17 00:00:00 2001
From: Haojian Wu <hokein.wu at gmail.com>
Date: Tue, 7 Apr 2026 09:17:16 +0200
Subject: [PATCH 1/3] [clang][Parser] Improve error recovery for missing
semicolons in class members.
---
clang/include/clang/Parse/Parser.h | 7 +++++++
clang/lib/Parse/ParseCXXInlineMethods.cpp | 3 ++-
clang/lib/Parse/ParseDeclCXX.cpp | 3 ++-
clang/lib/Parse/Parser.cpp | 11 +++++++++++
clang/test/AST/ast-dump-decl-recovery.cpp | 16 ++++++++++++++++
clang/test/Parser/recovery.cpp | 2 +-
6 files changed, 39 insertions(+), 3 deletions(-)
create mode 100644 clang/test/AST/ast-dump-decl-recovery.cpp
diff --git a/clang/include/clang/Parse/Parser.h b/clang/include/clang/Parse/Parser.h
index c077671cb2407..20afffc327443 100644
--- a/clang/include/clang/Parse/Parser.h
+++ b/clang/include/clang/Parse/Parser.h
@@ -815,6 +815,13 @@ class Parser : public CodeCompletionHandler {
/// to the semicolon, consumes that extra token.
bool ExpectAndConsumeSemi(unsigned DiagID, StringRef TokenUsed = "");
+ /// Try to inject a synthetic semicolon if the next token appears to be the
+ /// start of a new declaration (i.e., it starts a new line and is a
+ /// declaration specifier). This is used for error recovery to prevent
+ /// aggressive skipping and preserve subsequent declarations in the AST.
+ /// Returns true if a semicolon was injected.
+ bool TryInjectSemicolon();
+
/// Consume any extra semi-colons until the end of the line.
void ConsumeExtraSemi(ExtraSemiKind Kind, DeclSpec::TST T = TST_unspecified);
diff --git a/clang/lib/Parse/ParseCXXInlineMethods.cpp b/clang/lib/Parse/ParseCXXInlineMethods.cpp
index bc18881e89110..24724a430bc13 100644
--- a/clang/lib/Parse/ParseCXXInlineMethods.cpp
+++ b/clang/lib/Parse/ParseCXXInlineMethods.cpp
@@ -131,7 +131,8 @@ NamedDecl *Parser::ParseCXXInlineMethodDef(
<< Delete;
SkipUntil(tok::semi);
} else if (ExpectAndConsume(tok::semi, diag::err_expected_after,
- Delete ? "delete" : "default")) {
+ Delete ? "delete" : "default") &&
+ !TryInjectSemicolon()) {
SkipUntil(tok::semi);
}
diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp
index 5a7863506cc91..2b098168075c4 100644
--- a/clang/lib/Parse/ParseDeclCXX.cpp
+++ b/clang/lib/Parse/ParseDeclCXX.cpp
@@ -3244,7 +3244,8 @@ Parser::DeclGroupPtrTy Parser::ParseCXXClassMemberDeclaration(
}
if (ExpectSemi &&
- ExpectAndConsume(tok::semi, diag::err_expected_semi_decl_list)) {
+ ExpectAndConsume(tok::semi, diag::err_expected_semi_decl_list) &&
+ !TryInjectSemicolon()) {
// Skip to end of block or statement.
SkipUntil(tok::r_brace, StopAtSemi | StopBeforeMatch);
// If we stopped at a ';', eat it.
diff --git a/clang/lib/Parse/Parser.cpp b/clang/lib/Parse/Parser.cpp
index c4f745612e06c..c42b32b71be16 100644
--- a/clang/lib/Parse/Parser.cpp
+++ b/clang/lib/Parse/Parser.cpp
@@ -194,6 +194,17 @@ bool Parser::ExpectAndConsumeSemi(unsigned DiagID, StringRef TokenUsed) {
return ExpectAndConsume(tok::semi, DiagID , TokenUsed);
}
+bool Parser::TryInjectSemicolon() {
+ if (Tok.isAtStartOfLine() &&
+ (isDeclarationSpecifier(ImplicitTypenameContext::No))) {
+ PP.EnterToken(Tok, /*IsReinject=*/true);
+ Tok.setKind(tok::semi);
+ ConsumeToken();
+ return true;
+ }
+ return false;
+}
+
void Parser::ConsumeExtraSemi(ExtraSemiKind Kind, DeclSpec::TST TST) {
if (!Tok.is(tok::semi)) return;
diff --git a/clang/test/AST/ast-dump-decl-recovery.cpp b/clang/test/AST/ast-dump-decl-recovery.cpp
new file mode 100644
index 0000000000000..f6ad1de015dc3
--- /dev/null
+++ b/clang/test/AST/ast-dump-decl-recovery.cpp
@@ -0,0 +1,16 @@
+// RUN: not %clang_cc1 -triple x86_64-unknown-unknown -std=c++20 -ast-dump %s | FileCheck -strict-whitespace %s
+
+namespace MissingSemicolon {
+class Foo {
+ void f1() = delete
+ void g1();
+ void f2()
+ void g2();
+};
+// CHECK: NamespaceDecl {{.*}} MissingSemicolon
+// CHECK: CXXMethodDecl {{.*}} f1 'void ()' delete
+// CHECK: CXXMethodDecl {{.*}} g1 'void ()'
+// CHECK: CXXMethodDecl {{.*}} f2 'void ()'
+// CHECK: CXXMethodDecl {{.*}} g2 'void ()'
+
+} // namespace MissingSemicolon
diff --git a/clang/test/Parser/recovery.cpp b/clang/test/Parser/recovery.cpp
index 261f5dc99bad4..0637d4cbe72ae 100644
--- a/clang/test/Parser/recovery.cpp
+++ b/clang/test/Parser/recovery.cpp
@@ -24,7 +24,7 @@ struct S {
int a, b, c;
S();
int x // expected-error {{expected ';'}}
- friend void f()
+ friend void f() // expected-error {{expected ';' at end of declaration list}}
};
8S::S() : a{ 5 }, b{ 6 }, c{ 2 } { // expected-error {{unqualified-id}}
return;
>From 9694f96e3c2b7fc53b8abacbe69624d9210be333 Mon Sep 17 00:00:00 2001
From: Haojian Wu <hokein.wu at gmail.com>
Date: Sat, 11 Apr 2026 23:06:50 +0200
Subject: [PATCH 2/3] Simplify the change, no need to inject a token and
consume it.
---
clang/include/clang/Parse/Parser.h | 10 ++++------
clang/lib/Parse/ParseCXXInlineMethods.cpp | 2 +-
clang/lib/Parse/ParseDeclCXX.cpp | 2 +-
clang/lib/Parse/Parser.cpp | 12 +++---------
4 files changed, 9 insertions(+), 17 deletions(-)
diff --git a/clang/include/clang/Parse/Parser.h b/clang/include/clang/Parse/Parser.h
index 20afffc327443..e29c43f8751ec 100644
--- a/clang/include/clang/Parse/Parser.h
+++ b/clang/include/clang/Parse/Parser.h
@@ -815,12 +815,10 @@ class Parser : public CodeCompletionHandler {
/// to the semicolon, consumes that extra token.
bool ExpectAndConsumeSemi(unsigned DiagID, StringRef TokenUsed = "");
- /// Try to inject a synthetic semicolon if the next token appears to be the
- /// start of a new declaration (i.e., it starts a new line and is a
- /// declaration specifier). This is used for error recovery to prevent
- /// aggressive skipping and preserve subsequent declarations in the AST.
- /// Returns true if a semicolon was injected.
- bool TryInjectSemicolon();
+ /// Returns true if the current token is likely the start of a new
+ /// declaration (e.g., it starts a new line and is a declaration specifier).
+ /// This is a heuristic used for error recovery.
+ bool isLikelyAtStartOfNewDeclaration();
/// Consume any extra semi-colons until the end of the line.
void ConsumeExtraSemi(ExtraSemiKind Kind, DeclSpec::TST T = TST_unspecified);
diff --git a/clang/lib/Parse/ParseCXXInlineMethods.cpp b/clang/lib/Parse/ParseCXXInlineMethods.cpp
index 24724a430bc13..114df51aa08ba 100644
--- a/clang/lib/Parse/ParseCXXInlineMethods.cpp
+++ b/clang/lib/Parse/ParseCXXInlineMethods.cpp
@@ -132,7 +132,7 @@ NamedDecl *Parser::ParseCXXInlineMethodDef(
SkipUntil(tok::semi);
} else if (ExpectAndConsume(tok::semi, diag::err_expected_after,
Delete ? "delete" : "default") &&
- !TryInjectSemicolon()) {
+ !isLikelyAtStartOfNewDeclaration()) {
SkipUntil(tok::semi);
}
diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp
index 2b098168075c4..683a1b4695625 100644
--- a/clang/lib/Parse/ParseDeclCXX.cpp
+++ b/clang/lib/Parse/ParseDeclCXX.cpp
@@ -3245,7 +3245,7 @@ Parser::DeclGroupPtrTy Parser::ParseCXXClassMemberDeclaration(
if (ExpectSemi &&
ExpectAndConsume(tok::semi, diag::err_expected_semi_decl_list) &&
- !TryInjectSemicolon()) {
+ !isLikelyAtStartOfNewDeclaration()) {
// Skip to end of block or statement.
SkipUntil(tok::r_brace, StopAtSemi | StopBeforeMatch);
// If we stopped at a ';', eat it.
diff --git a/clang/lib/Parse/Parser.cpp b/clang/lib/Parse/Parser.cpp
index c42b32b71be16..5d87453cf219e 100644
--- a/clang/lib/Parse/Parser.cpp
+++ b/clang/lib/Parse/Parser.cpp
@@ -194,15 +194,9 @@ bool Parser::ExpectAndConsumeSemi(unsigned DiagID, StringRef TokenUsed) {
return ExpectAndConsume(tok::semi, DiagID , TokenUsed);
}
-bool Parser::TryInjectSemicolon() {
- if (Tok.isAtStartOfLine() &&
- (isDeclarationSpecifier(ImplicitTypenameContext::No))) {
- PP.EnterToken(Tok, /*IsReinject=*/true);
- Tok.setKind(tok::semi);
- ConsumeToken();
- return true;
- }
- return false;
+bool Parser::isLikelyAtStartOfNewDeclaration() {
+ return Tok.isAtStartOfLine() &&
+ isDeclarationSpecifier(ImplicitTypenameContext::No);
}
void Parser::ConsumeExtraSemi(ExtraSemiKind Kind, DeclSpec::TST TST) {
>From 0af7807667b60c60e4f97c23b99e20b976dd0b41 Mon Sep 17 00:00:00 2001
From: Haojian Wu <hokein.wu at gmail.com>
Date: Thu, 16 Apr 2026 09:04:27 +0200
Subject: [PATCH 3/3] Release note
---
clang/docs/ReleaseNotes.rst | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 5edec5f04e976..c320359f09c87 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -382,6 +382,10 @@ Improvements to Clang's diagnostics
code can automatically be made portable to other host platforms that don't
support backslashes.
+- Improved error recovery for missing semicolons after class members. Clang now avoids
+ skipping subsequent valid declarations when their previous decl is missing semicolon.
+
+
Improvements to Clang's time-trace
----------------------------------
More information about the cfe-commits
mailing list