Skip to content

[clang-format] Add ApplyAlwaysOnePerLineToTemplateArguments option #137544

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

LorenzoMauro
Copy link

Introduce a new FormatStyle option, ApplyAlwaysOnePerLineToTemplateArguments,
which controls whether BinPackParameters=AlwaysOnePerLine also applies to template argument lists.

This allows users to enforce one-per-line formatting for function parameters without unintentionally splitting template arguments.

Includes unit tests covering function declarations, definitions, and template instantiations, both with and without trailing comments.


Behavior

If BinPackParameters is set to AlwaysOnePerLine, this option controls whether template arguments are split across multiple lines.

  • When true, which is the current only behaviour, each template argument is placed on its own line:
template <typename T, int N>
struct Foo {
  T mData[N];

  Foo<T,
      N>
  operator+(const Foo<T,
                      N> &other) const {}

  Foo<T,
      N>
  bar(const Foo<T,
                N> &other,
      float t) const {}
};
  • When false, template argument lists remain compact even if function parameters are broken one per line:
template <typename T, int N>
struct Foo {
  T mData[N];

  Foo<T, N> operator+(const Foo<T, N> &other) const {}

  Foo<T, N> bar(const Foo<T, N> &other,
                float t) const {}
};

Copy link

Thank you for submitting a Pull Request (PR) to the LLVM Project!

This PR will be automatically labeled and the relevant teams will be notified.

If you wish to, you can add reviewers by using the "Reviewers" section on this page.

If this is not working for you, it is probably because you do not have write permissions for the repository. In which case you can instead tag reviewers by name in a comment by using @ followed by their GitHub username.

If you have received no comments on your PR for a week, you can request a review by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate is once a week. Please remember that you are asking for valuable time from other developers.

If you have further questions, they may be answered by the LLVM GitHub User Guide.

You can also ask questions in a comment on this PR, on the LLVM Discord or on the forums.

@llvmbot
Copy link
Member

llvmbot commented Apr 27, 2025

@llvm/pr-subscribers-clang

@llvm/pr-subscribers-clang-format

Author: Lorenzo Mauro (LorenzoMauro)

Changes

Introduce a new FormatStyle option, ApplyAlwaysOnePerLineToTemplateArguments,
which controls whether BinPackParameters=AlwaysOnePerLine also applies to template argument lists.

This allows users to enforce one-per-line formatting for function parameters without unintentionally splitting template arguments.

Includes unit tests covering function declarations, definitions, and template instantiations, both with and without trailing comments.


Behavior

If BinPackParameters is set to AlwaysOnePerLine, this option controls whether template arguments are split across multiple lines.

  • When true, which is the current only behaviour, each template argument is placed on its own line:
template &lt;typename T, int N&gt;
struct Foo {
  T mData[N];

  Foo&lt;T,
      N&gt;
  operator+(const Foo&lt;T,
                      N&gt; &amp;other) const {}

  Foo&lt;T,
      N&gt;
  bar(const Foo&lt;T,
                N&gt; &amp;other,
      float t) const {}
};
  • When false, template argument lists remain compact even if function parameters are broken one per line:
template &lt;typename T, int N&gt;
struct Foo {
  T mData[N];

  Foo&lt;T, N&gt; operator+(const Foo&lt;T, N&gt; &amp;other) const {}

  Foo&lt;T, N&gt; bar(const Foo&lt;T, N&gt; &amp;other,
                float t) const {}
};

Full diff: https://github.com/llvm/llvm-project/pull/137544.diff

8 Files Affected:

  • (modified) clang/include/clang/Format/Format.h (+26)
  • (modified) clang/lib/Format/Format.cpp (+3)
  • (modified) clang/lib/Format/FormatToken.h (+8-5)
  • (modified) clang/lib/Format/TokenAnnotator.cpp (+16)
  • (modified) clang/lib/Format/TokenAnnotator.h (+3)
  • (modified) clang/unittests/Format/ConfigParseTest.cpp (+6)
  • (modified) clang/unittests/Format/FormatTest.cpp (+49)
  • (modified) clang/unittests/Format/FormatTestComments.cpp (+60)
diff --git a/clang/include/clang/Format/Format.h b/clang/include/clang/Format/Format.h
index f6ceef08b46da..81dafb53b1e89 100644
--- a/clang/include/clang/Format/Format.h
+++ b/clang/include/clang/Format/Format.h
@@ -1259,6 +1259,30 @@ struct FormatStyle {
   /// \version 3.7
   BinPackParametersStyle BinPackParameters;
 
+  /// If ``BinPackParameters`` is set to ``AlwaysOnePerLine``, specifies whether
+  /// template argument lists should also be split across multiple lines.
+  ///
+  /// When set to ``true``, each template argument will be placed on its own
+  /// line. When set to ``false``, template argument lists remain compact even
+  /// when function parameters are broken one per line.
+  ///
+  /// \code
+  /// true:
+  /// template <
+  ///     typename T,
+  ///     typename U>
+  /// void foo(
+  ///     T a,
+  ///     U b);
+  ///
+  /// false:
+  /// template <typename T, typename U>
+  /// void foo(
+  ///     T a,
+  ///     U b);
+  /// \endcode
+  bool ApplyAlwaysOnePerLineToTemplateArguments = false;
+
   /// Styles for adding spacing around ``:`` in bitfield definitions.
   enum BitFieldColonSpacingStyle : int8_t {
     /// Add one space on each side of the ``:``
@@ -5326,6 +5350,8 @@ struct FormatStyle {
            BinPackArguments == R.BinPackArguments &&
            BinPackLongBracedList == R.BinPackLongBracedList &&
            BinPackParameters == R.BinPackParameters &&
+           ApplyAlwaysOnePerLineToTemplateArguments ==
+               R.ApplyAlwaysOnePerLineToTemplateArguments &&
            BitFieldColonSpacing == R.BitFieldColonSpacing &&
            BracedInitializerIndentWidth == R.BracedInitializerIndentWidth &&
            BreakAdjacentStringLiterals == R.BreakAdjacentStringLiterals &&
diff --git a/clang/lib/Format/Format.cpp b/clang/lib/Format/Format.cpp
index 5a1c3f556b331..13f3bc3db3cdc 100644
--- a/clang/lib/Format/Format.cpp
+++ b/clang/lib/Format/Format.cpp
@@ -1007,6 +1007,8 @@ template <> struct MappingTraits<FormatStyle> {
     IO.mapOptional("BinPackArguments", Style.BinPackArguments);
     IO.mapOptional("BinPackLongBracedList", Style.BinPackLongBracedList);
     IO.mapOptional("BinPackParameters", Style.BinPackParameters);
+    IO.mapOptional("ApplyAlwaysOnePerLineToTemplateArguments",
+                   Style.ApplyAlwaysOnePerLineToTemplateArguments);
     IO.mapOptional("BitFieldColonSpacing", Style.BitFieldColonSpacing);
     IO.mapOptional("BracedInitializerIndentWidth",
                    Style.BracedInitializerIndentWidth);
@@ -1521,6 +1523,7 @@ FormatStyle getLLVMStyle(FormatStyle::LanguageKind Language) {
   LLVMStyle.BinPackArguments = true;
   LLVMStyle.BinPackLongBracedList = true;
   LLVMStyle.BinPackParameters = FormatStyle::BPPS_BinPack;
+  LLVMStyle.ApplyAlwaysOnePerLineToTemplateArguments = true;
   LLVMStyle.BitFieldColonSpacing = FormatStyle::BFCS_Both;
   LLVMStyle.BracedInitializerIndentWidth = -1;
   LLVMStyle.BraceWrapping = {/*AfterCaseLabel=*/false,
diff --git a/clang/lib/Format/FormatToken.h b/clang/lib/Format/FormatToken.h
index 946cd7b81587f..7f15c48f15bc3 100644
--- a/clang/lib/Format/FormatToken.h
+++ b/clang/lib/Format/FormatToken.h
@@ -309,11 +309,11 @@ struct FormatToken {
         IsUnterminatedLiteral(false), CanBreakBefore(false),
         ClosesTemplateDeclaration(false), StartsBinaryExpression(false),
         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) {}
+        InTemplateArgumentList(false), ContinuesLineCommentSection(false),
+        Finalized(false), ClosesRequiresClause(false),
+        EndsCppAttributeGroup(false), BlockKind(BK_Unknown),
+        Decision(FD_Unformatted), PackingKind(PPK_Inconclusive),
+        TypeIsFinalized(false), Type(TT_Unknown) {}
 
   /// The \c Token.
   Token Tok;
@@ -373,6 +373,9 @@ struct FormatToken {
   /// Only set if \c Type == \c TT_StartOfName.
   unsigned PartOfMultiVariableDeclStmt : 1;
 
+  /// \c true if this token is part of a template argument list.
+  unsigned InTemplateArgumentList : 1;
+
   /// Does this line comment continue a line comment section?
   ///
   /// Only set to true if \c Type == \c TT_LineComment.
diff --git a/clang/lib/Format/TokenAnnotator.cpp b/clang/lib/Format/TokenAnnotator.cpp
index e56cc92987af7..b39c4b2bb609c 100644
--- a/clang/lib/Format/TokenAnnotator.cpp
+++ b/clang/lib/Format/TokenAnnotator.cpp
@@ -1977,6 +1977,19 @@ class AnnotatingParser {
     return Type;
   }
 
+  void markTokenAsTemplateArgumentInLine() {
+    int TemplateDepth = 0;
+    for (FormatToken *Tok = Line.First; Tok; Tok = Tok->Next) {
+      if (Tok->is(TT_TemplateCloser))
+        --TemplateDepth;
+
+      Tok->InTemplateArgumentList = (TemplateDepth > 0);
+
+      if (Tok->is(TT_TemplateOpener))
+        ++TemplateDepth;
+    }
+  }
+
 public:
   LineType parseLine() {
     if (!CurrentToken)
@@ -2074,6 +2087,7 @@ class AnnotatingParser {
       if (ctx.ContextType == Context::StructArrayInitializer)
         return LT_ArrayOfStructInitializer;
 
+    markTokenAsTemplateArgumentInLine();
     return LT_Other;
   }
 
@@ -5619,6 +5633,8 @@ bool TokenAnnotator::mustBreakBefore(const AnnotatedLine &Line,
   // BreakFunctionDefinitionParameters or AlignAfterOpenBracket.
   if (Style.BinPackParameters == FormatStyle::BPPS_AlwaysOnePerLine &&
       Line.MightBeFunctionDecl && !Left.opensScope() &&
+      (Style.ApplyAlwaysOnePerLineToTemplateArguments ||
+       !Left.InTemplateArgumentList) &&
       startsNextParameter(Right, Style)) {
     return true;
   }
diff --git a/clang/lib/Format/TokenAnnotator.h b/clang/lib/Format/TokenAnnotator.h
index e4b94431e68b4..4d72916dc64a7 100644
--- a/clang/lib/Format/TokenAnnotator.h
+++ b/clang/lib/Format/TokenAnnotator.h
@@ -186,6 +186,9 @@ class AnnotatedLine {
   bool MightBeFunctionDecl;
   bool IsMultiVariableDeclStmt;
 
+  /// \c True if this token is part o a template declaration.
+  bool InTemplateDecl = false;
+
   /// \c True if this line contains a macro call for which an expansion exists.
   bool ContainsMacroCall = false;
 
diff --git a/clang/unittests/Format/ConfigParseTest.cpp b/clang/unittests/Format/ConfigParseTest.cpp
index 2b08b794792e9..31f1cbe8e4bf7 100644
--- a/clang/unittests/Format/ConfigParseTest.cpp
+++ b/clang/unittests/Format/ConfigParseTest.cpp
@@ -482,6 +482,12 @@ TEST(ConfigParseTest, ParsesConfiguration) {
   CHECK_PARSE("BinPackParameters: false", BinPackParameters,
               FormatStyle::BPPS_OnePerLine);
 
+  Style.ApplyAlwaysOnePerLineToTemplateArguments = false;
+  CHECK_PARSE("ApplyAlwaysOnePerLineToTemplateArguments: true",
+              ApplyAlwaysOnePerLineToTemplateArguments, true);
+  CHECK_PARSE("ApplyAlwaysOnePerLineToTemplateArguments: false",
+              ApplyAlwaysOnePerLineToTemplateArguments, false);
+
   Style.PackConstructorInitializers = FormatStyle::PCIS_BinPack;
   CHECK_PARSE("PackConstructorInitializers: Never", PackConstructorInitializers,
               FormatStyle::PCIS_Never);
diff --git a/clang/unittests/Format/FormatTest.cpp b/clang/unittests/Format/FormatTest.cpp
index 333d40d481025..a50e1cc9b3d38 100644
--- a/clang/unittests/Format/FormatTest.cpp
+++ b/clang/unittests/Format/FormatTest.cpp
@@ -9024,6 +9024,55 @@ TEST_F(FormatTest, FormatsDeclarationBreakAlways) {
       BreakAlways);
 }
 
+TEST_F(FormatTest, ApplyAlwaysOnePerLineToTemplateArguments) {
+  FormatStyle Style = getGoogleStyle();
+  Style.BinPackParameters = FormatStyle::BPPS_AlwaysOnePerLine;
+
+  // Case 1: Template arguments split by AlwaysOnePerLine
+  Style.ApplyAlwaysOnePerLineToTemplateArguments = true;
+  verifyFormat("template <typename T, int N>\n"
+               "struct Foo {\n"
+               "  T mData[N];\n"
+               "  Foo<T,\n"
+               "      N>\n"
+               "  operator+(const Foo<T,\n"
+               "                      N> &other) const {}\n"
+               "  Foo<T,\n"
+               "      N>\n"
+               "  bar(const Foo<T,\n"
+               "                N> &other,\n"
+               "      float t) const {}\n"
+               "};\n",
+               Style);
+
+  // Case 2: Template arguments not split by The
+  // ApplyAlwaysOnePerLineToTemplateArguments
+  Style.ApplyAlwaysOnePerLineToTemplateArguments = false;
+  verifyFormat("template <typename T, int N>\n"
+               "struct Foo {\n"
+               "  T mData[N];\n"
+               "  Foo<T, N> operator+(const Foo<T, N> &other) const {}\n"
+               "  Foo<T, N> bar(const Foo<T, N> &other,\n"
+               "                float t) const {}\n"
+               "};\n",
+               Style);
+
+  // Case 3: Template arguments not split by the
+  // ApplyAlwaysOnePerLineToTemplateArguments but using the
+  // BreakFunctionDefinitionParameters flag
+  Style.BreakFunctionDefinitionParameters = true;
+  verifyFormat("template <typename T, int N>\n"
+               "struct Foo {\n"
+               "  T mData[N];\n"
+               "  Foo<T, N> operator+(\n"
+               "      const Foo<T, N> &other) const {}\n"
+               "  Foo<T, N> bar(\n"
+               "      const Foo<T, N> &other,\n"
+               "      float t) const {}\n"
+               "};\n",
+               Style);
+}
+
 TEST_F(FormatTest, FormatsDefinitionBreakAlways) {
   FormatStyle BreakAlways = getGoogleStyle();
   BreakAlways.BinPackParameters = FormatStyle::BPPS_AlwaysOnePerLine;
diff --git a/clang/unittests/Format/FormatTestComments.cpp b/clang/unittests/Format/FormatTestComments.cpp
index 5eefd767706a3..9b04a3cfc7553 100644
--- a/clang/unittests/Format/FormatTestComments.cpp
+++ b/clang/unittests/Format/FormatTestComments.cpp
@@ -444,6 +444,66 @@ TEST_F(FormatTestComments, UnderstandsBlockComments) {
                      "  int jjj; /*b*/");
 }
 
+TEST_F(FormatTestComments,
+       AlwaysOnePerLineRespectsTemplateArgumentsFlagWithComments) {
+  FormatStyle Style = getGoogleStyle();
+  Style.BinPackParameters = FormatStyle::BPPS_AlwaysOnePerLine;
+
+  // Case 1: Template arguments split by AlwaysOnePerLine
+  Style.ApplyAlwaysOnePerLineToTemplateArguments = true;
+  verifyFormat("template <typename T,  // comment\n"
+               "          int N>       // comment\n"
+               "struct Foo {\n"
+               "  T mData[N];\n"
+               "  Foo<T,\n"
+               "      N>\n"
+               "  operator+(const Foo<T,\n"
+               "                      N> &other) const {  // comment\n"
+               "  }\n"
+               "  Foo<T,\n"
+               "      N>\n"
+               "  bar(const Foo<T,\n"
+               "                N> &other,  // comment\n"
+               "      float t) const {      // comment\n"
+               "  }\n"
+               "};\n",
+               Style);
+
+  // Case 2: Template arguments not split by The
+  // ApplyAlwaysOnePerLineToTemplateArguments
+  Style.ApplyAlwaysOnePerLineToTemplateArguments = false;
+  verifyFormat(
+      "template <typename T,  // comment\n"
+      "          int N>       // comment\n"
+      "struct Foo {\n"
+      "  T mData[N];\n"
+      "  Foo<T, N> operator+(const Foo<T, N> &other) const {  // comment\n"
+      "  }\n"
+      "  Foo<T, N> bar(const Foo<T, N> &other,  // comment\n"
+      "                float t) const {         // comment\n"
+      "  }\n"
+      "};\n",
+      Style);
+
+  // Case 3: Template arguments not split by the
+  // ApplyAlwaysOnePerLineToTemplateArguments but using the
+  // BreakFunctionDefinitionParameters flag
+  Style.BreakFunctionDefinitionParameters = true;
+  verifyFormat("template <typename T,  // comment\n"
+               "          int N>       // comment\n"
+               "struct Foo {\n"
+               "  T mData[N];\n"
+               "  Foo<T, N> operator+(\n"
+               "      const Foo<T, N> &other) const {  // comment\n"
+               "  }\n"
+               "  Foo<T, N> bar(\n"
+               "      const Foo<T, N> &other,  // comment\n"
+               "      float t) const {         // comment\n"
+               "  }\n"
+               "};\n",
+               Style);
+}
+
 TEST_F(FormatTestComments, AlignsBlockComments) {
   EXPECT_EQ("/*\n"
             " * Really multi-line\n"

@LorenzoMauro
Copy link
Author

Thanks for taking a look!
Happy to adjust anything if needed.

Introduce a new FormatStyle option, ApplyAlwaysOnePerLineToTemplateArguments,
which controls whether BinPackParameters=AlwaysOnePerLine also applies to template
argument lists. This allows users to enforce one-per-line for function parameters
without unintentionally splitting template parameters.

Includes unit tests covering both function declarations and definitions, with and
without trailing comments.
@llvmbot llvmbot added the clang Clang issues not falling into any other category label Apr 27, 2025
/// };
/// \endcode
/// \version 21
bool ApplyAlwaysOnePerLineToTemplateArguments;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sort alphabetically.

@@ -1259,6 +1259,45 @@ struct FormatStyle {
/// \version 3.7
BinPackParametersStyle BinPackParameters;

/// If ``BinPackParameters`` is set to ``AlwaysOnePerLine``, specifies whether
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a reference on BinPackParameters to this one.

@@ -5326,6 +5365,8 @@ struct FormatStyle {
BinPackArguments == R.BinPackArguments &&
BinPackLongBracedList == R.BinPackLongBracedList &&
BinPackParameters == R.BinPackParameters &&
ApplyAlwaysOnePerLineToTemplateArguments ==
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also sorting.

@@ -2074,6 +2087,7 @@ class AnnotatingParser {
if (ctx.ContextType == Context::StructArrayInitializer)
return LT_ArrayOfStructInitializer;

markTokenAsTemplateArgumentInLine();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you want this before the preceding loop. And can you get it into the first pass of the line? Just going through all tokens again doesn't seem nice.

@@ -186,6 +186,9 @@ class AnnotatedLine {
bool MightBeFunctionDecl;
bool IsMultiVariableDeclStmt;

/// \c True if this token is part o a template declaration.
bool InTemplateDecl = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A leftover?

@@ -9024,6 +9024,55 @@ TEST_F(FormatTest, FormatsDeclarationBreakAlways) {
BreakAlways);
}

TEST_F(FormatTest, ApplyAlwaysOnePerLineToTemplateArguments) {
FormatStyle Style = getGoogleStyle();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why google style?

@@ -444,6 +444,66 @@ TEST_F(FormatTestComments, UnderstandsBlockComments) {
" int jjj; /*b*/");
}

TEST_F(FormatTestComments,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's your reason for this test? You didn't change anything comment related, right?

@owenca owenca requested a review from mydeveloperday April 29, 2025 04:58
@owenca
Copy link
Contributor

owenca commented Apr 29, 2025

Introduce a new FormatStyle option, ApplyAlwaysOnePerLineToTemplateArguments, which controls whether BinPackParameters=AlwaysOnePerLine also applies to template argument lists.

This allows users to enforce one-per-line formatting for function parameters without unintentionally splitting template arguments.

This looks like a bug. Can we fix it without introducing a new option? If not, can we add it as a sub-option to BinPackParameters?

@LorenzoMauro
Copy link
Author

Thank you @HazardyKnusperkeks for the review.

@owenca I agree — I also don't see why anyone would want the current behavior, so it definitely seems like a bug to me as well.
It's absolutely possible to fix this without introducing a new option: the check I wrote could simply apply unconditionally when BinPackParameters is set to AlwaysOnePerLine.

If we take that approach, most of the changes in the PR would also become unnecessary.

Let me know how you'd like to proceed, and I’ll update the patch accordingly.

@owenca
Copy link
Contributor

owenca commented May 1, 2025

I don't like new options for bug fixes. WDYT @mydeveloperday?

@rmarker
Copy link
Contributor

rmarker commented May 7, 2025

It seems like BinPackParameters is currently applying to both regular parameters and also template parameters.
I think it would be inconsistent if it stopped applying to template parameters only when set to AlwaysOnePerLine.

What about separating it out into BinPackTemplateParameters so that the two cases can be controlled independently?
There are already BinPackArguments and BinPackLongBracedList also, so having multiple options for bin packing different situations is already established.

I imagine that backwards compatibility could be maintained by having BinPackTemplateParameters use the BinPackParameters if not explicitly set.

Copy link
Contributor

@mydeveloperday mydeveloperday left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the current behaviour vs the default you have introduced?

@@ -1521,6 +1523,7 @@ FormatStyle getLLVMStyle(FormatStyle::LanguageKind Language) {
LLVMStyle.BinPackArguments = true;
LLVMStyle.BinPackLongBracedList = true;
LLVMStyle.BinPackParameters = FormatStyle::BPPS_BinPack;
LLVMStyle.ApplyAlwaysOnePerLineToTemplateArguments = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the current behavior? is this the correct default?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang Clang issues not falling into any other category clang-format
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants