[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