[clang] [clang-format] add option to control bin-packing keyworded parameters (PR #131605)
Eugene Shalygin via cfe-commits
cfe-commits at lists.llvm.org
Mon Mar 31 01:39:43 PDT 2025
https://github.com/zeule updated https://github.com/llvm/llvm-project/pull/131605
>From e2db7bd178a2c466b066787f235578b624a23644 Mon Sep 17 00:00:00 2001
From: Eugene Shalygin <e.shalygin at abberior-instruments.com>
Date: Mon, 17 Mar 2025 11:23:35 +0100
Subject: [PATCH] [clang-format] option to control bin-packing keyworded
parameters
The Q_PROPERTY declaration is almost like a function declaration, but
uses keywords as parameter separators. This allows users to provide list
of those keywords to be used to control bin-packing of the macro
parameters.
---
clang/docs/ClangFormatStyleOptions.rst | 32 ++++++++++
clang/docs/tools/dump_format_style.py | 1 +
clang/docs/tools/plurals.txt | 1 +
clang/include/clang/Format/Format.h | 40 +++++++++++++
clang/lib/Format/ContinuationIndenter.cpp | 4 ++
clang/lib/Format/Format.cpp | 11 ++++
clang/lib/Format/FormatToken.cpp | 2 +
clang/lib/Format/FormatToken.h | 1 +
clang/lib/Format/TokenAnnotator.cpp | 50 ++++++++++++++--
clang/unittests/Format/ConfigParseTest.cpp | 10 ++++
clang/unittests/Format/FormatTest.cpp | 59 +++++++++++++++++++
clang/unittests/Format/TokenAnnotatorTest.cpp | 33 +++++++++++
12 files changed, 240 insertions(+), 4 deletions(-)
diff --git a/clang/docs/ClangFormatStyleOptions.rst b/clang/docs/ClangFormatStyleOptions.rst
index 9ecac68ae72bf..d318fc3295c32 100644
--- a/clang/docs/ClangFormatStyleOptions.rst
+++ b/clang/docs/ClangFormatStyleOptions.rst
@@ -4740,6 +4740,38 @@ the configuration (without a prefix: ``Auto``).
replaced with a single newline and form feed followed by the remaining
newlines.
+.. _KeywordedFunctionLikeMacros:
+
+**KeywordedFunctionLikeMacros** (``List of KeywordedFunctionLikeMacros``) :versionbadge:`clang-format 21` :ref:`¶ <KeywordedFunctionLikeMacros>`
+ Allows to format function-like macros with keyworded parameters according
+ to the BinPackParameters setting, treating keywords as parameter
+ sepratators.
+
+ Q_PROPERTY is an example of such a macro:
+
+ .. code-block:: c++
+
+ Q_PROPERTY(int name READ name WRITE setName NOTIFY nameChanged)
+
+ With ``BinPackParameters`` set to ``OnePerLine`` (or
+ ``AlwaysOnePerLine``) and
+
+ .. code-block:: yaml
+
+ KeywordedFunctionLikeMacros:
+ - Name: "Q_PROPERTY"
+ Keywords: ['READ', 'WRITE', 'MEMBER', 'RESET', 'NOTIFY']
+
+ the line above will be split on these keywords:
+
+ .. code-block:: c++
+
+ Q_PROPERTY(
+ int name
+ READ name
+ WRITE setName
+ NOTIFY nameChanged)
+
.. _LambdaBodyIndentation:
**LambdaBodyIndentation** (``LambdaBodyIndentationKind``) :versionbadge:`clang-format 13` :ref:`¶ <LambdaBodyIndentation>`
diff --git a/clang/docs/tools/dump_format_style.py b/clang/docs/tools/dump_format_style.py
index f035143f6b3d1..85732af8e0a60 100755
--- a/clang/docs/tools/dump_format_style.py
+++ b/clang/docs/tools/dump_format_style.py
@@ -462,6 +462,7 @@ class State:
"std::string",
"std::vector<std::string>",
"std::vector<IncludeCategory>",
+ "std::vector<KeywordedFunctionLikeMacro>",
"std::vector<RawStringFormat>",
"std::optional<unsigned>",
"deprecated",
diff --git a/clang/docs/tools/plurals.txt b/clang/docs/tools/plurals.txt
index e20b7f970ba43..bd08c65df1c52 100644
--- a/clang/docs/tools/plurals.txt
+++ b/clang/docs/tools/plurals.txt
@@ -1,3 +1,4 @@
Strings
IncludeCategories
+KeywordedFunctionLikeMacros
RawStringFormats
diff --git a/clang/include/clang/Format/Format.h b/clang/include/clang/Format/Format.h
index fec47a248abb4..5ba27adb053a3 100644
--- a/clang/include/clang/Format/Format.h
+++ b/clang/include/clang/Format/Format.h
@@ -3276,6 +3276,45 @@ struct FormatStyle {
/// \version 20
bool KeepFormFeed;
+ /// Function-like declaration with keyworded parameters.
+ /// Lists possible keywords for a named function-like macro.
+ struct KeywordedFunctionLikeMacro {
+ std::string Name;
+ std::vector<std::string> Keywords;
+
+ bool operator==(const KeywordedFunctionLikeMacro &Other) const {
+ return Name == Other.Name && Keywords == Other.Keywords;
+ }
+ };
+
+ /// Allows to format function-like macros with keyworded parameters according
+ /// to the BinPackParameters setting, treating keywords as parameter
+ /// separators.
+ ///
+ /// Q_PROPERTY is an example of such a macro:
+ /// \code
+ /// Q_PROPERTY(int name READ name WRITE setName NOTIFY nameChanged)
+ /// \endcode
+ ///
+ /// With ``BinPackParameters`` set to ``OnePerLine`` (or
+ /// ``AlwaysOnePerLine``) and
+ /// \code{.yaml}
+ /// KeywordedFunctionLikeMacros:
+ /// - Name: "Q_PROPERTY"
+ /// Keywords: ['READ', 'WRITE', 'MEMBER', 'RESET', 'NOTIFY']
+ /// \endcode
+ ///
+ /// the line above will be split on these keywords:
+ /// \code
+ /// Q_PROPERTY(
+ /// int name
+ /// READ name
+ /// WRITE setName
+ /// NOTIFY nameChanged)
+ /// \endcode
+ /// \version 21
+ std::vector<KeywordedFunctionLikeMacro> KeywordedFunctionLikeMacros;
+
/// Indentation logic for lambda bodies.
enum LambdaBodyIndentationKind : int8_t {
/// Align lambda body relative to the lambda signature. This is the default.
@@ -5352,6 +5391,7 @@ struct FormatStyle {
JavaScriptWrapImports == R.JavaScriptWrapImports &&
KeepEmptyLines == R.KeepEmptyLines &&
KeepFormFeed == R.KeepFormFeed && Language == R.Language &&
+ KeywordedFunctionLikeMacros == R.KeywordedFunctionLikeMacros &&
LambdaBodyIndentation == R.LambdaBodyIndentation &&
LineEnding == R.LineEnding && MacroBlockBegin == R.MacroBlockBegin &&
MacroBlockEnd == R.MacroBlockEnd && Macros == R.Macros &&
diff --git a/clang/lib/Format/ContinuationIndenter.cpp b/clang/lib/Format/ContinuationIndenter.cpp
index 1969f4297b211..dd4297de73ac0 100644
--- a/clang/lib/Format/ContinuationIndenter.cpp
+++ b/clang/lib/Format/ContinuationIndenter.cpp
@@ -349,6 +349,10 @@ bool ContinuationIndenter::canBreak(const LineState &State) {
}
}
+ // Don't break between function parameter keywords and parameter names.
+ if (Previous.is(TT_FunctionParameterKeyword) && Current.is(TT_StartOfName))
+ return false;
+
// Don't allow breaking before a closing brace of a block-indented braced list
// initializer if there isn't already a break.
if (Current.is(tok::r_brace) && Current.MatchingParen &&
diff --git a/clang/lib/Format/Format.cpp b/clang/lib/Format/Format.cpp
index 28aea86139e0d..198458e9f5d4c 100644
--- a/clang/lib/Format/Format.cpp
+++ b/clang/lib/Format/Format.cpp
@@ -28,6 +28,7 @@
using clang::format::FormatStyle;
+LLVM_YAML_IS_SEQUENCE_VECTOR(FormatStyle::KeywordedFunctionLikeMacro)
LLVM_YAML_IS_SEQUENCE_VECTOR(FormatStyle::RawStringFormat)
namespace llvm {
@@ -399,6 +400,14 @@ template <> struct MappingTraits<FormatStyle::KeepEmptyLinesStyle> {
}
};
+template <> struct MappingTraits<FormatStyle::KeywordedFunctionLikeMacro> {
+ static void mapping(IO &IO,
+ FormatStyle::KeywordedFunctionLikeMacro &Function) {
+ IO.mapOptional("Name", Function.Name);
+ IO.mapOptional("Keywords", Function.Keywords);
+ }
+};
+
template <> struct ScalarEnumerationTraits<FormatStyle::LanguageKind> {
static void enumeration(IO &IO, FormatStyle::LanguageKind &Value) {
IO.enumCase(Value, "C", FormatStyle::LK_C);
@@ -1072,6 +1081,8 @@ template <> struct MappingTraits<FormatStyle> {
IO.mapOptional("JavaScriptWrapImports", Style.JavaScriptWrapImports);
IO.mapOptional("KeepEmptyLines", Style.KeepEmptyLines);
IO.mapOptional("KeepFormFeed", Style.KeepFormFeed);
+ IO.mapOptional("KeywordedFunctionLikeMacros",
+ Style.KeywordedFunctionLikeMacros);
IO.mapOptional("LambdaBodyIndentation", Style.LambdaBodyIndentation);
IO.mapOptional("LineEnding", Style.LineEnding);
IO.mapOptional("MacroBlockBegin", Style.MacroBlockBegin);
diff --git a/clang/lib/Format/FormatToken.cpp b/clang/lib/Format/FormatToken.cpp
index 7752139142430..28a49124cf21f 100644
--- a/clang/lib/Format/FormatToken.cpp
+++ b/clang/lib/Format/FormatToken.cpp
@@ -331,6 +331,8 @@ bool startsNextParameter(const FormatToken &Current, const FormatStyle &Style) {
}
if (Style.Language == FormatStyle::LK_Proto && Current.is(TT_SelectorName))
return true;
+ if (Current.is(TT_FunctionParameterKeyword))
+ return true;
return Previous.is(tok::comma) && !Current.isTrailingComment() &&
((Previous.isNot(TT_CtorInitializerComma) ||
Style.BreakConstructorInitializers !=
diff --git a/clang/lib/Format/FormatToken.h b/clang/lib/Format/FormatToken.h
index 3808872d227a9..5580b5ab95ab7 100644
--- a/clang/lib/Format/FormatToken.h
+++ b/clang/lib/Format/FormatToken.h
@@ -84,6 +84,7 @@ namespace format {
TYPE(FunctionDeclarationLParen) \
TYPE(FunctionLBrace) \
TYPE(FunctionLikeOrFreestandingMacro) \
+ TYPE(FunctionParameterKeyword) \
TYPE(FunctionTypeLParen) \
/* The colons as part of a C11 _Generic selection */ \
TYPE(GenericSelectionColon) \
diff --git a/clang/lib/Format/TokenAnnotator.cpp b/clang/lib/Format/TokenAnnotator.cpp
index d87b3a6088bd8..23ab27b8ede35 100644
--- a/clang/lib/Format/TokenAnnotator.cpp
+++ b/clang/lib/Format/TokenAnnotator.cpp
@@ -116,6 +116,16 @@ static bool isCppAttribute(bool IsCpp, const FormatToken &Tok) {
return AttrTok && AttrTok->startsSequence(tok::r_square, tok::r_square);
}
+static bool isParametersKeyword(
+ const FormatToken &Tok,
+ const FormatStyle::KeywordedFunctionLikeMacro *declaration) {
+ if (!declaration)
+ return false;
+
+ return std::find(declaration->Keywords.begin(), declaration->Keywords.end(),
+ Tok.TokenText) != declaration->Keywords.end();
+}
+
/// A parser that gathers additional information about tokens.
///
/// The \c TokenAnnotator tries to match parenthesis and square brakets and
@@ -148,6 +158,32 @@ class AnnotatingParser {
}
}
+ const FormatStyle::KeywordedFunctionLikeMacro *
+ findKeywordedFunctionLikeMacro() const {
+ const FormatToken *TokBeforeFirstLParent = nullptr;
+ for (const FormatToken *T = Line.First; T != Line.Last; T = T->Next) {
+ if (T->Tok.is(tok::l_paren)) {
+ TokBeforeFirstLParent = T->getPreviousNonComment();
+ break;
+ }
+ }
+
+ // Unknown if line ends with ';', FunctionLikeOrFreestandingMacro otherwise.
+ if (!TokBeforeFirstLParent ||
+ !TokBeforeFirstLParent->isOneOf(TT_FunctionLikeOrFreestandingMacro,
+ TT_Unknown)) {
+ return nullptr;
+ }
+ auto I = std::find_if(
+ Style.KeywordedFunctionLikeMacros.begin(),
+ Style.KeywordedFunctionLikeMacros.end(),
+ [TokBeforeFirstLParent](
+ const FormatStyle::KeywordedFunctionLikeMacro &Declaration) {
+ return TokBeforeFirstLParent->TokenText == Declaration.Name;
+ });
+ return I != Style.KeywordedFunctionLikeMacros.end() ? &*I : nullptr;
+ }
+
bool parseAngle() {
if (!CurrentToken)
return false;
@@ -2415,8 +2451,12 @@ class AnnotatingParser {
Current.setType(TT_BinaryOperator);
} else if (isStartOfName(Current) &&
(!Line.MightBeFunctionDecl || Current.NestingLevel != 0)) {
- Contexts.back().FirstStartOfName = &Current;
- Current.setType(TT_StartOfName);
+ if (isParametersKeyword(Current, findKeywordedFunctionLikeMacro())) {
+ Current.setType(TT_FunctionParameterKeyword);
+ } else {
+ Contexts.back().FirstStartOfName = &Current;
+ Current.setType(TT_StartOfName);
+ }
} else if (Current.is(tok::semi)) {
// Reset FirstStartOfName after finding a semicolon so that a for loop
// with multiple increment statements is not confused with a for loop
@@ -3783,6 +3823,7 @@ void TokenAnnotator::annotate(AnnotatedLine &Line) {
static bool isFunctionDeclarationName(const LangOptions &LangOpts,
const FormatToken &Current,
const AnnotatedLine &Line,
+ const FormatStyle &Style,
FormatToken *&ClosingParen) {
if (Current.is(TT_FunctionDeclarationName))
return true;
@@ -3993,7 +4034,7 @@ void TokenAnnotator::calculateFormattingInformation(AnnotatedLine &Line) const {
AfterLastAttribute = Tok;
if (const bool IsCtorOrDtor = Tok->is(TT_CtorDtorDeclName);
IsCtorOrDtor ||
- isFunctionDeclarationName(LangOpts, *Tok, Line, ClosingParen)) {
+ isFunctionDeclarationName(LangOpts, *Tok, Line, Style, ClosingParen)) {
if (!IsCtorOrDtor)
Tok->setFinalizedType(TT_FunctionDeclarationName);
LineIsFunctionDeclaration = true;
@@ -6231,7 +6272,8 @@ bool TokenAnnotator::canBreakBefore(const AnnotatedLine &Line,
Right.Next->isOneOf(TT_FunctionDeclarationName, tok::kw_const)));
}
if (Right.isOneOf(TT_StartOfName, TT_FunctionDeclarationName,
- TT_ClassHeadName, tok::kw_operator)) {
+ TT_FunctionParameterKeyword, TT_ClassHeadName,
+ tok::kw_operator)) {
return true;
}
if (Right.isAttribute())
diff --git a/clang/unittests/Format/ConfigParseTest.cpp b/clang/unittests/Format/ConfigParseTest.cpp
index 287191d04d885..b7c1755c66cf0 100644
--- a/clang/unittests/Format/ConfigParseTest.cpp
+++ b/clang/unittests/Format/ConfigParseTest.cpp
@@ -1105,6 +1105,16 @@ TEST(ConfigParseTest, ParsesConfiguration) {
FormatStyle::SDS_Leave);
CHECK_PARSE("SeparateDefinitionBlocks: Never", SeparateDefinitionBlocks,
FormatStyle::SDS_Never);
+
+ Style.KeywordedFunctionLikeMacros.clear();
+ std::vector<FormatStyle::KeywordedFunctionLikeMacro> ExpectedFunctions = {
+ {"MACRO_A", {"PARAM1", "KEYWORD", "KW_2"}}, {"macro", {"mKW1", "mKW2"}}};
+ CHECK_PARSE("KeywordedFunctionLikeMacros:\n"
+ " - Name: MACRO_A\n"
+ " Keywords: ['PARAM1', 'KEYWORD', 'KW_2']\n"
+ " - Name: macro\n"
+ " Keywords: [ \"mKW1\", \"mKW2\" ]\n",
+ KeywordedFunctionLikeMacros, ExpectedFunctions);
}
TEST(ConfigParseTest, ParsesConfigurationWithLanguages) {
diff --git a/clang/unittests/Format/FormatTest.cpp b/clang/unittests/Format/FormatTest.cpp
index 0b90bd360b758..4104b99754d78 100644
--- a/clang/unittests/Format/FormatTest.cpp
+++ b/clang/unittests/Format/FormatTest.cpp
@@ -29102,6 +29102,65 @@ TEST_F(FormatTest, BreakBeforeClassName) {
" ArenaSafeUniquePtr {};");
}
+TEST_F(FormatTest, KeywordedFunctionLikeMacros) {
+ FormatStyle::KeywordedFunctionLikeMacro QPropertyDeclaration;
+ QPropertyDeclaration.Name = "Q_PROPERTY";
+ QPropertyDeclaration.Keywords.push_back("READ");
+ QPropertyDeclaration.Keywords.push_back("WRITE");
+ QPropertyDeclaration.Keywords.push_back("NOTIFY");
+ QPropertyDeclaration.Keywords.push_back("RESET");
+
+ auto Style40 = getLLVMStyleWithColumns(40);
+ Style40.KeywordedFunctionLikeMacros.push_back(QPropertyDeclaration);
+ Style40.BinPackParameters = FormatStyle::BPPS_OnePerLine;
+
+ verifyFormat("Q_PROPERTY(int name\n"
+ " READ name\n"
+ " WRITE setName\n"
+ " NOTIFY nameChanged)",
+ Style40);
+ verifyFormat("class A {\n"
+ " Q_PROPERTY(int name\n"
+ " READ name\n"
+ " WRITE setName\n"
+ " NOTIFY nameChanged)\n"
+ "};",
+ Style40);
+ verifyFormat("/* sdf */ Q_PROPERTY(int name\n"
+ " READ name\n"
+ " WRITE setName\n"
+ " NOTIFY nameChanged)",
+ Style40);
+
+ auto Style120 = getLLVMStyleWithColumns(120);
+ Style120.KeywordedFunctionLikeMacros.push_back(QPropertyDeclaration);
+ Style120.BinPackParameters = FormatStyle::BPPS_AlwaysOnePerLine;
+
+ verifyFormat("Q_PROPERTY(int name\n"
+ " READ name\n"
+ " WRITE setName\n"
+ " NOTIFY nameChanged)",
+ Style120);
+ verifyFormat("class A {\n"
+ " Q_PROPERTY(int name\n"
+ " READ name\n"
+ " WRITE setName\n"
+ " NOTIFY nameChanged)\n"
+ "};",
+ Style120);
+
+ Style120.BinPackParameters = FormatStyle::BPPS_BinPack;
+
+ verifyFormat(
+ "Q_PROPERTY(int name READ name WRITE setName NOTIFY nameChanged)",
+ Style120);
+
+ Style120.BinPackParameters = FormatStyle::BPPS_OnePerLine;
+ verifyFormat(
+ "Q_PROPERTY(int name READ name WRITE setName NOTIFY nameChanged)",
+ Style120);
+}
+
} // namespace
} // namespace test
} // namespace format
diff --git a/clang/unittests/Format/TokenAnnotatorTest.cpp b/clang/unittests/Format/TokenAnnotatorTest.cpp
index ac5e979aea071..4223c82cbbb2f 100644
--- a/clang/unittests/Format/TokenAnnotatorTest.cpp
+++ b/clang/unittests/Format/TokenAnnotatorTest.cpp
@@ -3926,6 +3926,39 @@ TEST_F(TokenAnnotatorTest, UserDefinedConversionFunction) {
EXPECT_TOKEN(Tokens[5], tok::l_paren, TT_FunctionDeclarationLParen);
}
+TEST_F(TokenAnnotatorTest, KeywordedFunctionLikeMacro) {
+ auto Style = getLLVMStyle();
+ FormatStyle::KeywordedFunctionLikeMacro QPropertyDeclaration;
+ QPropertyDeclaration.Name = "Q_PROPERTY";
+ QPropertyDeclaration.Keywords.push_back("READ");
+ QPropertyDeclaration.Keywords.push_back("WRITE");
+ QPropertyDeclaration.Keywords.push_back("NOTIFY");
+ QPropertyDeclaration.Keywords.push_back("RESET");
+ Style.KeywordedFunctionLikeMacros.push_back(QPropertyDeclaration);
+
+ auto Tokens = annotate(
+ "Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)",
+ Style);
+ ASSERT_EQ(Tokens.size(), 12u) << Tokens;
+ EXPECT_TOKEN(Tokens[0], tok::identifier, TT_Unknown);
+ EXPECT_TOKEN(Tokens[4], tok::identifier, TT_FunctionParameterKeyword);
+ EXPECT_TOKEN(Tokens[5], tok::identifier, TT_StartOfName);
+ EXPECT_TOKEN(Tokens[6], tok::identifier, TT_FunctionParameterKeyword);
+ EXPECT_TOKEN(Tokens[7], tok::identifier, TT_StartOfName);
+ EXPECT_TOKEN(Tokens[8], tok::identifier, TT_FunctionParameterKeyword);
+ EXPECT_TOKEN(Tokens[9], tok::identifier, TT_StartOfName);
+
+ Tokens = annotate(
+ "struct S { Q_OBJECT\n Q_PROPERTY(int value READ value WRITE setValue "
+ "NOTIFY valueChanged)\n };",
+ Style);
+ ASSERT_EQ(Tokens.size(), 18u) << Tokens;
+ EXPECT_TOKEN(Tokens[4], tok::identifier, TT_FunctionLikeOrFreestandingMacro);
+ EXPECT_TOKEN(Tokens[8], tok::identifier, TT_FunctionParameterKeyword);
+ EXPECT_TOKEN(Tokens[10], tok::identifier, TT_FunctionParameterKeyword);
+ EXPECT_TOKEN(Tokens[12], tok::identifier, TT_FunctionParameterKeyword);
+}
+
} // namespace
} // namespace format
} // namespace clang
More information about the cfe-commits
mailing list