[clang] [clang-format] Option to insert spaces before the closing `*/` (PR #162105)
via cfe-commits
cfe-commits at lists.llvm.org
Wed Oct 8 00:00:35 PDT 2025
https://github.com/Men-cotton updated https://github.com/llvm/llvm-project/pull/162105
>From 975bc0bf794096813d3aff6962f5580456b3d689 Mon Sep 17 00:00:00 2001
From: mencotton <mencotton0410 at gmail.com>
Date: Tue, 7 Oct 2025 23:14:20 +0900
Subject: [PATCH] [clang-format] Add option to insert space before closing
block comments
---
clang/docs/ClangFormatStyleOptions.rst | 11 ++++
clang/include/clang/Format/Format.h | 10 ++++
clang/lib/Format/BreakableToken.cpp | 51 ++++++++++++++++++-
clang/lib/Format/Format.cpp | 3 ++
clang/lib/Format/FormatToken.h | 14 +++--
clang/lib/Format/FormatTokenLexer.cpp | 17 +++++++
clang/lib/Format/TokenAnnotator.cpp | 5 +-
clang/unittests/Format/FormatTestComments.cpp | 47 +++++++++++++++++
8 files changed, 153 insertions(+), 5 deletions(-)
diff --git a/clang/docs/ClangFormatStyleOptions.rst b/clang/docs/ClangFormatStyleOptions.rst
index b746df5dab264..70582b6c40980 100644
--- a/clang/docs/ClangFormatStyleOptions.rst
+++ b/clang/docs/ClangFormatStyleOptions.rst
@@ -6343,6 +6343,17 @@ the configuration (without a prefix: ``Auto``).
case 1 : break; case 1: break;
} }
+.. _SpaceBeforeClosingBlockComment:
+
+**SpaceBeforeClosingBlockComment** (``Boolean``) :versionbadge:`clang-format 21` :ref:`¶ <SpaceBeforeClosingBlockComment>`
+ If ``true``, a space is inserted immediately before the closing ``*/`` in
+ block comments that contain content.
+
+ .. code-block:: c++
+
+ true: false:
+ /* comment */ vs. /* comment*/
+
.. _SpaceBeforeCpp11BracedList:
**SpaceBeforeCpp11BracedList** (``Boolean``) :versionbadge:`clang-format 7` :ref:`¶ <SpaceBeforeCpp11BracedList>`
diff --git a/clang/include/clang/Format/Format.h b/clang/include/clang/Format/Format.h
index 3df5b92654094..7136fd2c5a4f8 100644
--- a/clang/include/clang/Format/Format.h
+++ b/clang/include/clang/Format/Format.h
@@ -4684,6 +4684,15 @@ struct FormatStyle {
/// \version 17
bool SpaceBeforeJsonColon;
+ /// If ``true``, a space is inserted immediately before the closing ``*/`` in
+ /// block comments that contain content.
+ /// \code
+ /// true: false:
+ /// /* comment */ vs. /* comment*/
+ /// \endcode
+ /// \version 21
+ bool SpaceBeforeClosingBlockComment;
+
/// Different ways to put a space before opening parentheses.
enum SpaceBeforeParensStyle : int8_t {
/// This is **deprecated** and replaced by ``Custom`` below, with all
@@ -5611,6 +5620,7 @@ struct FormatStyle {
SpaceAroundPointerQualifiers == R.SpaceAroundPointerQualifiers &&
SpaceBeforeRangeBasedForLoopColon ==
R.SpaceBeforeRangeBasedForLoopColon &&
+ SpaceBeforeClosingBlockComment == R.SpaceBeforeClosingBlockComment &&
SpaceBeforeSquareBrackets == R.SpaceBeforeSquareBrackets &&
SpaceInEmptyBraces == R.SpaceInEmptyBraces &&
SpacesBeforeTrailingComments == R.SpacesBeforeTrailingComments &&
diff --git a/clang/lib/Format/BreakableToken.cpp b/clang/lib/Format/BreakableToken.cpp
index 29db20067c361..ba41ba6f3f6cb 100644
--- a/clang/lib/Format/BreakableToken.cpp
+++ b/clang/lib/Format/BreakableToken.cpp
@@ -776,6 +776,32 @@ void BreakableBlockComment::reflow(unsigned LineIndex,
void BreakableBlockComment::adaptStartOfLine(
unsigned LineIndex, WhitespaceManager &Whitespaces) const {
if (LineIndex == 0) {
+ if (Style.SpaceBeforeClosingBlockComment) {
+ const StringRef Content = Tok.TokenText;
+ if (Content.size() >= 4 && Content.starts_with("/*") &&
+ Content.ends_with("*/")) {
+ const StringRef Body = Content.drop_front(2).drop_back(2);
+ const bool IsEffectivelyEmpty = Body.find_if_not([](char c) {
+ return c == ' ' || c == '\t';
+ }) == StringRef::npos;
+ const bool IsSingleLine = !Content.contains('\n');
+
+ if (IsEffectivelyEmpty && IsSingleLine) {
+ const unsigned WhitespaceLength = Body.size();
+ Whitespaces.replaceWhitespaceInToken(
+ Tok, /*Offset=*/2, WhitespaceLength, /*PreviousPostfix=*/"",
+ /*CurrentPrefix=*/"", InPPDirective, /*Newlines=*/1,
+ /*Spaces=*/0);
+ } else if (Tok.NeedsSpaceBeforeClosingBlockComment &&
+ Tok.SpaceBeforeClosingBlockCommentOffset <
+ Tok.TokenText.size()) {
+ Whitespaces.replaceWhitespaceInToken(
+ Tok, Tok.SpaceBeforeClosingBlockCommentOffset,
+ /*ReplaceChars=*/0, /*PreviousPostfix=*/"", /*CurrentPrefix=*/" ",
+ InPPDirective, /*Newlines=*/0, /*Spaces=*/0);
+ }
+ }
+ }
if (DelimitersOnNewline) {
// Since we're breaking at index 1 below, the break position and the
// break length are the same.
@@ -816,9 +842,32 @@ void BreakableBlockComment::adaptStartOfLine(
unsigned WhitespaceLength = Content[LineIndex].data() -
tokenAt(LineIndex).TokenText.data() -
WhitespaceOffsetInToken;
+ int Spaces = ContentColumn[LineIndex] - Prefix.size();
+ const bool isTerminatorOnSeparateLine =
+ Content[LineIndex].ltrim(Blanks).empty();
+ const bool isLastLineOfBlock = (LineIndex + 1 == Lines.size());
+
+ if (isTerminatorOnSeparateLine && isLastLineOfBlock) {
+ if (Style.SpaceBeforeClosingBlockComment &&
+ !Tok.NeedsSpaceBeforeClosingBlockComment) {
+ // This case handles empty or whitespace-only comments like `/*\n*/`.
+ // The user wants a space before `*/`, but the lexer correctly identifies
+ // no space is needed for the token itself. We must avoid indenting the
+ // `*/` on its new line, which would result in an unwanted leading space.
+ StringRef Body = Tok.TokenText.drop_front(2).drop_back(2);
+ if (Body.trim().empty())
+ Spaces = 0;
+ } else if (Tok.NeedsSpaceBeforeClosingBlockComment && Spaces > 0) {
+ // The token text itself will contain the leading space (e.g., " */").
+ // The calculated `Spaces` is for alignment relative to the star column.
+ // Decrement to prevent adding a second, redundant space.
+ --Spaces;
+ }
+ }
+
Whitespaces.replaceWhitespaceInToken(
tokenAt(LineIndex), WhitespaceOffsetInToken, WhitespaceLength, "", Prefix,
- InPPDirective, /*Newlines=*/1, ContentColumn[LineIndex] - Prefix.size());
+ InPPDirective, /*Newlines=*/1, Spaces);
}
BreakableToken::Split
diff --git a/clang/lib/Format/Format.cpp b/clang/lib/Format/Format.cpp
index 686e54128d372..06292c75f27e0 100644
--- a/clang/lib/Format/Format.cpp
+++ b/clang/lib/Format/Format.cpp
@@ -1222,6 +1222,8 @@ template <> struct MappingTraits<FormatStyle> {
IO.mapOptional("SpaceBeforeInheritanceColon",
Style.SpaceBeforeInheritanceColon);
IO.mapOptional("SpaceBeforeJsonColon", Style.SpaceBeforeJsonColon);
+ IO.mapOptional("SpaceBeforeClosingBlockComment",
+ Style.SpaceBeforeClosingBlockComment);
IO.mapOptional("SpaceBeforeParens", Style.SpaceBeforeParens);
IO.mapOptional("SpaceBeforeParensOptions", Style.SpaceBeforeParensOptions);
IO.mapOptional("SpaceBeforeRangeBasedForLoopColon",
@@ -1717,6 +1719,7 @@ FormatStyle getLLVMStyle(FormatStyle::LanguageKind Language) {
LLVMStyle.SpaceBeforeCtorInitializerColon = true;
LLVMStyle.SpaceBeforeInheritanceColon = true;
LLVMStyle.SpaceBeforeJsonColon = false;
+ LLVMStyle.SpaceBeforeClosingBlockComment = false;
LLVMStyle.SpaceBeforeParens = FormatStyle::SBPO_ControlStatements;
LLVMStyle.SpaceBeforeParensOptions = {};
LLVMStyle.SpaceBeforeParensOptions.AfterControlStatements = true;
diff --git a/clang/lib/Format/FormatToken.h b/clang/lib/Format/FormatToken.h
index f015d27bed6af..0e0d407c37519 100644
--- a/clang/lib/Format/FormatToken.h
+++ b/clang/lib/Format/FormatToken.h
@@ -324,9 +324,9 @@ struct FormatToken {
EndsBinaryExpression(false), PartOfMultiVariableDeclStmt(false),
ContinuesLineCommentSection(false), Finalized(false),
ClosesRequiresClause(false), EndsCppAttributeGroup(false),
- BlockKind(BK_Unknown), Decision(FD_Unformatted),
- PackingKind(PPK_Inconclusive), TypeIsFinalized(false),
- Type(TT_Unknown) {}
+ NeedsSpaceBeforeClosingBlockComment(false), BlockKind(BK_Unknown),
+ Decision(FD_Unformatted), PackingKind(PPK_Inconclusive),
+ TypeIsFinalized(false), Type(TT_Unknown) {}
/// The \c Token.
Token Tok;
@@ -402,6 +402,9 @@ struct FormatToken {
/// \c true if this token ends a group of C++ attributes.
unsigned EndsCppAttributeGroup : 1;
+ /// \c true if clang-format should insert a space before the closing '*/'.
+ unsigned NeedsSpaceBeforeClosingBlockComment : 1;
+
private:
/// Contains the kind of block if this token is a brace.
unsigned BlockKind : 2;
@@ -505,6 +508,11 @@ struct FormatToken {
/// token.
unsigned LastLineColumnWidth = 0;
+ /// Offset (from the start of the token) where a space should be inserted
+ /// before the closing '*/' when \c NeedsSpaceBeforeClosingBlockComment is
+ /// set.
+ unsigned SpaceBeforeClosingBlockCommentOffset = 0;
+
/// The number of spaces that should be inserted before this token.
unsigned SpacesRequiredBefore = 0;
diff --git a/clang/lib/Format/FormatTokenLexer.cpp b/clang/lib/Format/FormatTokenLexer.cpp
index 86a5185a92a52..cbc71cd2767db 100644
--- a/clang/lib/Format/FormatTokenLexer.cpp
+++ b/clang/lib/Format/FormatTokenLexer.cpp
@@ -1386,6 +1386,23 @@ FormatToken *FormatTokenLexer::getNextToken() {
StringRef UntrimmedText = FormatTok->TokenText;
FormatTok->TokenText = FormatTok->TokenText.rtrim(" \t\v\f");
TrailingWhitespace = UntrimmedText.size() - FormatTok->TokenText.size();
+
+ bool NeedsSpace = true;
+ if (!Style.SpaceBeforeClosingBlockComment ||
+ !FormatTok->TokenText.starts_with("/*") ||
+ !FormatTok->TokenText.ends_with("*/")) {
+ NeedsSpace = false;
+ }
+ const StringRef Content =
+ FormatTok->TokenText.drop_front(2).drop_back(2).rtrim("\r\n");
+ if (Content.empty())
+ NeedsSpace = false;
+ const unsigned char LastChar = static_cast<unsigned char>(Content.back());
+ if (NeedsSpace && !isHorizontalWhitespace(LastChar)) {
+ FormatTok->NeedsSpaceBeforeClosingBlockComment = true;
+ FormatTok->SpaceBeforeClosingBlockCommentOffset =
+ FormatTok->TokenText.size() - 2;
+ }
} else if (FormatTok->is(tok::raw_identifier)) {
IdentifierInfo &Info = IdentTable.get(FormatTok->TokenText);
FormatTok->Tok.setIdentifierInfo(&Info);
diff --git a/clang/lib/Format/TokenAnnotator.cpp b/clang/lib/Format/TokenAnnotator.cpp
index 5b784eded4601..80aa34c855493 100644
--- a/clang/lib/Format/TokenAnnotator.cpp
+++ b/clang/lib/Format/TokenAnnotator.cpp
@@ -4821,7 +4821,10 @@ bool TokenAnnotator::spaceRequiredBetween(const AnnotatedLine &Line,
}
if (Left.is(TT_BlockComment)) {
// No whitespace in x(/*foo=*/1), except for JavaScript.
- return Style.isJavaScript() || !Left.TokenText.ends_with("=*/");
+ bool EndsWithAssignmentComment = Left.TokenText.ends_with("=*/");
+ if (!EndsWithAssignmentComment && Style.SpaceBeforeClosingBlockComment)
+ EndsWithAssignmentComment = Left.TokenText.ends_with("= */");
+ return Style.isJavaScript() || !EndsWithAssignmentComment;
}
// Space between template and attribute.
diff --git a/clang/unittests/Format/FormatTestComments.cpp b/clang/unittests/Format/FormatTestComments.cpp
index 69026bce98705..92ab32da9a2ba 100644
--- a/clang/unittests/Format/FormatTestComments.cpp
+++ b/clang/unittests/Format/FormatTestComments.cpp
@@ -332,6 +332,53 @@ TEST_F(FormatTestComments, UnderstandsSingleLineComments) {
verifyNoCrash(StringRef("/*\\\0\n/", 6));
}
+TEST_F(FormatTestComments, InsertsSpaceBeforeClosingBlockComment) {
+ FormatStyle Style = getLLVMStyle();
+ Style.SpaceBeforeClosingBlockComment = true;
+
+ verifyFormat("foo(/* comment */);", "foo(/* comment*/);", Style);
+ verifyFormat("foo(/*Logger= */nullptr);", "foo(/*Logger=*/nullptr);", Style);
+ verifyFormat("/* comment */", Style);
+ verifyFormat("/* comment before code */\nint x;",
+ "/* comment before code*/\nint x;", Style);
+ verifyFormat("/* comment\n */", "/* comment\n*/", Style);
+ verifyFormat("/* comment\n */\nint x;", "/* comment\n*/\nint x;", Style);
+ verifyFormat("/*\ncomment line\n */", "/*\ncomment line\n*/", Style);
+ verifyFormat("/*\n * comment star\n */", "/*\n * comment star\n*/", Style);
+ verifyFormat("/* comment line\nnext */", "/* comment line\nnext*/", Style);
+ verifyFormat("/*\n*/", Style);
+ verifyFormat("/*\n\n*/", "/*\n \n*/", Style);
+ verifyFormat("/*This is a multi line comment\n"
+ "this is the next line\n"
+ "and this is the 3th line. */",
+ "/*This is a multi line comment\n"
+ "this is the next line\n"
+ "and this is the 3th line.*/",
+ Style);
+ verifyFormat(
+ "/*\n * Another multi line comment\n * this is the next line. */",
+ "/*\n * Another multi line comment\n * this is the next line.*/", Style);
+}
+
+TEST_F(FormatTestComments, BlockCommentDoesNotForceBreakBeforeFollowingToken) {
+ FormatStyle Style = getLLVMStyle();
+ Style.SpaceBeforeClosingBlockComment = true;
+
+ verifyFormat("switch (n) {\n"
+ "case 1:\n"
+ " foo();\n"
+ "/*FALLTHROUGH */ case 2:\n"
+ " bar();\n"
+ "}\n",
+ "switch (n) {\n"
+ "case 1:\n"
+ " foo();\n"
+ " /*FALLTHROUGH*/ case 2:\n"
+ " bar();\n"
+ "}\n",
+ Style);
+}
+
TEST_F(FormatTestComments, KeepsParameterWithTrailingCommentsOnTheirOwnLine) {
EXPECT_EQ("SomeFunction(a,\n"
" b, // comment\n"
More information about the cfe-commits
mailing list