[clang] [clang-format] Option to insert spaces before the closing `*/` (PR #162105)

via cfe-commits cfe-commits at lists.llvm.org
Tue Oct 7 19:48:39 PDT 2025


https://github.com/Men-cotton updated https://github.com/llvm/llvm-project/pull/162105

>From ed55a3b0e862e1a278ffa4703bc388f04adc9929 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           | 42 ++++++++++++++++-
 clang/lib/Format/Format.cpp                   |  3 ++
 clang/lib/Format/FormatToken.h                | 14 ++++--
 clang/lib/Format/FormatTokenLexer.cpp         | 16 +++++++
 clang/lib/Format/TokenAnnotator.cpp           |  5 +-
 clang/unittests/Format/FormatTestComments.cpp | 47 +++++++++++++++++++
 8 files changed, 143 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..48feaeb06d186 100644
--- a/clang/lib/Format/BreakableToken.cpp
+++ b/clang/lib/Format/BreakableToken.cpp
@@ -776,6 +776,30 @@ void BreakableBlockComment::reflow(unsigned LineIndex,
 void BreakableBlockComment::adaptStartOfLine(
     unsigned LineIndex, WhitespaceManager &Whitespaces) const {
   if (LineIndex == 0) {
+    if (Style.SpaceBeforeClosingBlockComment) {
+      StringRef Body = Tok.TokenText;
+      Body = Body.drop_front(std::min<size_t>(2, Body.size()));
+      Body = Body.drop_back(std::min<size_t>(2, Body.size()));
+      bool BodyHasContent =
+          Body.find_first_not_of(" \t\v\f\r\n") != StringRef::npos;
+      bool HasNewline = Tok.TokenText.contains('\n');
+      if (!BodyHasContent && !HasNewline) {
+        unsigned ReplaceChars =
+            Tok.TokenText.size() >= 4 ? Tok.TokenText.size() - 4 : 0;
+        Whitespaces.replaceWhitespaceInToken(
+            Tok, /*Offset=*/2, ReplaceChars, /*PreviousPostfix=*/"",
+            /*CurrentPrefix=*/"", InPPDirective, /*Newlines=*/1,
+            /*Spaces=*/0);
+      }
+    }
+    if (Style.SpaceBeforeClosingBlockComment &&
+        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 +840,25 @@ void BreakableBlockComment::adaptStartOfLine(
   unsigned WhitespaceLength = Content[LineIndex].data() -
                               tokenAt(LineIndex).TokenText.data() -
                               WhitespaceOffsetInToken;
+  int Spaces = ContentColumn[LineIndex] - Prefix.size();
+  const bool LastLineHasNoContent = Content[LineIndex].ltrim(Blanks).empty();
+  if (!Tok.NeedsSpaceBeforeClosingBlockComment && LastLineHasNoContent &&
+      LineIndex + 1 == Lines.size()) {
+    const bool HasExistingSpaceBeforeClosing =
+        Tok.TokenText.size() >= 3 &&
+        isHorizontalWhitespace(static_cast<unsigned char>(
+            Tok.TokenText[Tok.TokenText.size() - 3]));
+    if (!HasExistingSpaceBeforeClosing)
+      Spaces = 0;
+  }
+  if (Tok.NeedsSpaceBeforeClosingBlockComment &&
+      Tok.SpaceBeforeClosingBlockCommentOffset < Tok.TokenText.size() &&
+      LineIndex + 1 == Lines.size() && Spaces > 0 && LastLineHasNoContent) {
+    --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..374caa4c1020c 100644
--- a/clang/lib/Format/FormatTokenLexer.cpp
+++ b/clang/lib/Format/FormatTokenLexer.cpp
@@ -1386,6 +1386,22 @@ FormatToken *FormatTokenLexer::getNextToken() {
     StringRef UntrimmedText = FormatTok->TokenText;
     FormatTok->TokenText = FormatTok->TokenText.rtrim(" \t\v\f");
     TrailingWhitespace = UntrimmedText.size() - FormatTok->TokenText.size();
+    if (Style.SpaceBeforeClosingBlockComment &&
+        FormatTok->TokenText.starts_with("/*") &&
+        FormatTok->TokenText.ends_with("*/")) {
+      StringRef Body = FormatTok->TokenText.drop_front(2).drop_back(2);
+      if (!Body.empty()) {
+        while (!Body.empty() && (Body.back() == '\n' || Body.back() == '\r'))
+          Body = Body.drop_back();
+        const bool BodyEmptyAfterStrippingNewlines = Body.empty();
+        if (!BodyEmptyAfterStrippingNewlines &&
+            !isHorizontalWhitespace(static_cast<unsigned char>(Body.back()))) {
+          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