[clang] [clang-format] Allow one indent level before explicit access labels (PR #227704)

via cfe-commits cfe-commits at lists.llvm.org
Wed Sep 30 06:19:35 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-clang-format

Author: n0ma (noambouillet)

<details>
<summary>Changes</summary>

## Summary

With `IndentAccessModifiers: true`, clang-format currently indents record members two levels even when the record has no explicit access label. 

Add `IndentImplicitAccessModifiers`, defaulting to `true`, to preserve that output.
Setting it to `false` indents members before the first explicit `public:`, `protected:`, or `private:` by one level. Members following the first label retain the existing two-level indentation.
Each record tracks its own access labels, so a label in a nested record does not change its parent. Qt access labels follow the same rule. The new behavior is limited to C-family parsing; Java formatting remains unchanged.

```yaml
IndentAccessModifiers: true
IndentImplicitAccessModifiers: false
```

This makes the layout in #<!-- -->61631 possible without changing existing configurations. It also addresses the related requests in #<!-- -->54333 and #<!-- -->182566.

## Tests

- Built `clang-format` and `FormatTests` from LLVM main.
- Ran all 1,295 `FormatTests`; all passed.
- Confirmed the new setting formats the #<!-- -->61631 reproduction exactly as requested and a second pass makes no changes.

## Design point for review

If a record has members before its first explicit access label, those members use one level, even when an access label appears later.
Reviewers may prefer a different option name or rule for that case.

## Development note

AI assisted with the fix as I'm not used to the codebase. I reviewed the resulting patch and tests before submitting this PR.


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


9 Files Affected:

- (modified) clang/docs/ClangFormatStyleOptions.md (+14-3) 
- (modified) clang/docs/ReleaseNotes.md (+4) 
- (modified) clang/include/clang/Format/Format.h (+13-3) 
- (modified) clang/lib/Format/Format.cpp (+3) 
- (modified) clang/lib/Format/UnwrappedLineParser.cpp (+36-11) 
- (modified) clang/lib/Format/UnwrappedLineParser.h (+4-2) 
- (modified) clang/unittests/Format/ConfigParseTest.cpp (+1) 
- (modified) clang/unittests/Format/FormatTest.cpp (+53) 
- (modified) clang/unittests/Format/FormatTestJava.cpp (+12) 


``````````diff
diff --git a/clang/docs/ClangFormatStyleOptions.md b/clang/docs/ClangFormatStyleOptions.md
index 81984ff185e53..d831bcac07c9c 100644
--- a/clang/docs/ClangFormatStyleOptions.md
+++ b/clang/docs/ClangFormatStyleOptions.md
@@ -4752,9 +4752,10 @@ the configuration (without a prefix: `Auto`).
   the record members, respecting the `AccessModifierOffset`. Record
   members are indented one level below the record.
   When `true`, access modifiers get their own indentation level. As a
-  consequence, record members are always indented 2 levels below the record,
-  regardless of the access modifier presence. Value of the
-  `AccessModifierOffset` is ignored.
+  consequence, record members are by default indented 2 levels below the
+  record, regardless of the access modifier presence. Value of the
+  `AccessModifierOffset` is ignored. `IndentImplicitAccessModifiers` can
+  change the indentation before the first explicit access modifier.
 
   ```c++
   false:                                 true:
@@ -4952,6 +4953,16 @@ the configuration (without a prefix: `Auto`).
 
 
 
+(indentimplicitaccessmodifiers)=
+
+**IndentImplicitAccessModifiers** (`Boolean`) {versionbadge}`clang-format 24` {ref}`¶ <IndentImplicitAccessModifiers>`
+
+: When `IndentAccessModifiers` is `true`, indent members before the first
+  explicit access modifier by two levels. Set this option to `false` to
+  indent those members by one level. Members after an explicit access
+  modifier still use two levels. This option has no effect if
+  `IndentAccessModifiers` is false.
+
 (indentppdirectives)=
 
 **IndentPPDirectives** (`PPDirectiveIndentStyle`) {versionbadge}`clang-format 6` {ref}`¶ <IndentPPDirectives>`
diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index c778703e8cc6f..a2090bb98df30 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -944,6 +944,10 @@ features cannot lower the translation-unit ABI level;
 
 ### clang-format
 
+- Add `IndentImplicitAccessModifiers` to allow members before the first
+  explicit access modifier to use one indentation level when
+  `IndentAccessModifiers` is enabled. The default preserves existing formatting.
+
 - Add `SpacesInBlockComments` option to control spacing after `/*` and
   before `*/` in ordinary block comments.
 - Add `AfterRequiresExpression` sub-option of `BraceWrapping` to wrap the
diff --git a/clang/include/clang/Format/Format.h b/clang/include/clang/Format/Format.h
index 6d4fa6e8ee2a7..40ae52a0148e8 100644
--- a/clang/include/clang/Format/Format.h
+++ b/clang/include/clang/Format/Format.h
@@ -3187,9 +3187,10 @@ struct FormatStyle {
   /// the record members, respecting the `AccessModifierOffset`. Record
   /// members are indented one level below the record.
   /// When `true`, access modifiers get their own indentation level. As a
-  /// consequence, record members are always indented 2 levels below the record,
-  /// regardless of the access modifier presence. Value of the
-  /// `AccessModifierOffset` is ignored.
+  /// consequence, record members are by default indented 2 levels below the
+  /// record, regardless of the access modifier presence. Value of the
+  /// `AccessModifierOffset` is ignored. `IndentImplicitAccessModifiers` can
+  /// change the indentation before the first explicit access modifier.
   /// \code
   ///    false:                                 true:
   ///    class C {                      vs.     class C {
@@ -3208,6 +3209,14 @@ struct FormatStyle {
   /// \version 13
   bool IndentAccessModifiers;
 
+  /// When `IndentAccessModifiers` is `true`, indent members before the first
+  /// explicit access modifier by two levels. Set this option to `false` to
+  /// indent those members by one level. Members after an explicit access
+  /// modifier still use two levels. This option has no effect if
+  /// `IndentAccessModifiers` is false.
+  /// \version 24
+  bool IndentImplicitAccessModifiers;
+
   /// Indent case label blocks one level from the case label.
   ///
   /// When `false`, the block following the case label uses the same
@@ -6242,6 +6251,7 @@ struct FormatStyle {
                R.IncludeStyle.IncludeIsMainSourceRegex &&
            IncludeStyle.MainIncludeChar == R.IncludeStyle.MainIncludeChar &&
            IndentAccessModifiers == R.IndentAccessModifiers &&
+           IndentImplicitAccessModifiers == R.IndentImplicitAccessModifiers &&
            IndentCaseBlocks == R.IndentCaseBlocks &&
            IndentCaseLabels == R.IndentCaseLabels &&
            IndentExportBlock == R.IndentExportBlock &&
diff --git a/clang/lib/Format/Format.cpp b/clang/lib/Format/Format.cpp
index 4c78c1dbe9f80..2f6d70d2bf4e2 100644
--- a/clang/lib/Format/Format.cpp
+++ b/clang/lib/Format/Format.cpp
@@ -1399,6 +1399,8 @@ template <> struct MappingTraits<FormatStyle> {
     IO.mapOptional("IncludeIsMainSourceRegex",
                    Style.IncludeStyle.IncludeIsMainSourceRegex);
     IO.mapOptional("IndentAccessModifiers", Style.IndentAccessModifiers);
+    IO.mapOptional("IndentImplicitAccessModifiers",
+                   Style.IndentImplicitAccessModifiers);
     IO.mapOptional("IndentCaseBlocks", Style.IndentCaseBlocks);
     IO.mapOptional("IndentCaseLabels", Style.IndentCaseLabels);
     IO.mapOptional("IndentExportBlock", Style.IndentExportBlock);
@@ -1977,6 +1979,7 @@ FormatStyle getLLVMStyle(FormatStyle::LanguageKind Language) {
   LLVMStyle.IncludeStyle.IncludeIsMainRegex = "(Test)?$";
   LLVMStyle.IncludeStyle.MainIncludeChar = tooling::IncludeStyle::MICD_Quote;
   LLVMStyle.IndentAccessModifiers = false;
+  LLVMStyle.IndentImplicitAccessModifiers = true;
   LLVMStyle.IndentCaseBlocks = false;
   LLVMStyle.IndentCaseLabels = false;
   LLVMStyle.IndentExportBlock = true;
diff --git a/clang/lib/Format/UnwrappedLineParser.cpp b/clang/lib/Format/UnwrappedLineParser.cpp
index 4825e825af1fa..0e0825112b513 100644
--- a/clang/lib/Format/UnwrappedLineParser.cpp
+++ b/clang/lib/Format/UnwrappedLineParser.cpp
@@ -352,7 +352,8 @@ bool UnwrappedLineParser::precededByCommentOrPPDirective() const {
 /// (A simple block has a single statement.)
 bool UnwrappedLineParser::parseLevel(const FormatToken *OpeningBrace,
                                      IfStmtKind *IfKind,
-                                     FormatToken **IfLeftBrace) {
+                                     FormatToken **IfLeftBrace,
+                                     bool *SeenExplicitAccessModifier) {
   const bool InRequiresExpression =
       OpeningBrace && OpeningBrace->is(TT_RequiresExpressionLBrace);
   const bool IsPrecededByCommentOrPPDirective =
@@ -377,7 +378,18 @@ bool UnwrappedLineParser::parseLevel(const FormatToken *OpeningBrace,
       Kind = tok::r_brace;
 
     auto ParseDefault = [this, OpeningBrace, IfKind, &IfLBrace, &HasDoWhile,
-                         &HasLabel, &StatementCount] {
+                         &HasLabel, &StatementCount,
+                         SeenExplicitAccessModifier] {
+      const bool IsQtAccessLabel =
+          SeenExplicitAccessModifier && !*SeenExplicitAccessModifier &&
+          FormatTok->isOneOf(Keywords.kw_signals, Keywords.kw_qsignals,
+                             Keywords.kw_slots, Keywords.kw_qslots) &&
+          Tokens->peekNextToken(/*SkipComment=*/true)->is(tok::colon);
+      if (SeenExplicitAccessModifier && !*SeenExplicitAccessModifier &&
+          (FormatTok->isAccessSpecifierKeyword() || IsQtAccessLabel)) {
+        ++Line->Level;
+        *SeenExplicitAccessModifier = true;
+      }
       parseStructuralElement(OpeningBrace, IfKind, &IfLBrace,
                              HasDoWhile ? nullptr : &HasDoWhile,
                              HasLabel ? nullptr : &HasLabel);
@@ -744,11 +756,10 @@ bool UnwrappedLineParser::mightFitOnOneLine(
   return Line.Level * Style.IndentWidth + Length <= ColumnLimit;
 }
 
-FormatToken *UnwrappedLineParser::parseBlock(bool MustBeDeclaration,
-                                             unsigned AddLevels, bool MunchSemi,
-                                             bool KeepBraces,
-                                             IfStmtKind *IfKind,
-                                             bool UnindentWhitesmithsBraces) {
+FormatToken *UnwrappedLineParser::parseBlock(
+    bool MustBeDeclaration, unsigned AddLevels, bool MunchSemi, bool KeepBraces,
+    IfStmtKind *IfKind, bool UnindentWhitesmithsBraces,
+    bool IndentAfterExplicitAccessModifier) {
   auto HandleVerilogBlockLabel = [this]() {
     // ":" name
     if (Style.isVerilog() && FormatTok->is(tok::colon)) {
@@ -820,7 +831,11 @@ FormatToken *UnwrappedLineParser::parseBlock(bool MustBeDeclaration,
     Line->Level += AddLevels - (IsWhitesmiths ? 1 : 0);
 
   FormatToken *IfLBrace = nullptr;
-  const bool SimpleBlock = parseLevel(Tok, IfKind, &IfLBrace);
+  bool SeenExplicitAccessModifier = false;
+  const bool SimpleBlock =
+      parseLevel(Tok, IfKind, &IfLBrace,
+                 IndentAfterExplicitAccessModifier ? &SeenExplicitAccessModifier
+                                                   : nullptr);
 
   if (eof())
     return IfLBrace;
@@ -879,7 +894,8 @@ FormatToken *UnwrappedLineParser::parseBlock(bool MustBeDeclaration,
   size_t PPEndHash = computePPHash();
 
   // Munch the closing brace.
-  nextToken(/*LevelDifference=*/-AddLevels);
+  nextToken(/*LevelDifference=*/
+            -static_cast<int>(AddLevels + SeenExplicitAccessModifier));
 
   // When this is a function block and there is an unnecessary semicolon
   // afterwards then mark it as optional (so the RemoveSemi pass can get rid of
@@ -4298,8 +4314,17 @@ void UnwrappedLineParser::parseRecord(bool ParseAsExpr, bool IsJavaRecord) {
         addUnwrappedLine();
       }
 
-      unsigned AddLevels = Style.IndentAccessModifiers ? 2u : 1u;
-      parseBlock(/*MustBeDeclaration=*/true, AddLevels, /*MunchSemi=*/false);
+      const bool IndentAfterExplicitAccessModifier =
+          Style.isCpp() && Style.IndentAccessModifiers &&
+          !Style.IndentImplicitAccessModifiers;
+      unsigned AddLevels =
+          Style.IndentAccessModifiers && !IndentAfterExplicitAccessModifier
+              ? 2u
+              : 1u;
+      parseBlock(/*MustBeDeclaration=*/true, AddLevels, /*MunchSemi=*/false,
+                 /*KeepBraces=*/true, /*IfKind=*/nullptr,
+                 /*UnindentWhitesmithsBraces=*/false,
+                 IndentAfterExplicitAccessModifier);
     }
     setPreviousRBraceType(ClosingBraceType);
   }
diff --git a/clang/lib/Format/UnwrappedLineParser.h b/clang/lib/Format/UnwrappedLineParser.h
index 5b93c8f346d75..2e1755ad3b2c6 100644
--- a/clang/lib/Format/UnwrappedLineParser.h
+++ b/clang/lib/Format/UnwrappedLineParser.h
@@ -126,13 +126,15 @@ class UnwrappedLineParser {
   bool precededByCommentOrPPDirective() const;
   bool parseLevel(const FormatToken *OpeningBrace = nullptr,
                   IfStmtKind *IfKind = nullptr,
-                  FormatToken **IfLeftBrace = nullptr);
+                  FormatToken **IfLeftBrace = nullptr,
+                  bool *SeenExplicitAccessModifier = nullptr);
   bool mightFitOnOneLine(UnwrappedLine &Line,
                          const FormatToken *OpeningBrace = nullptr) const;
   FormatToken *parseBlock(bool MustBeDeclaration = false,
                           unsigned AddLevels = 1u, bool MunchSemi = true,
                           bool KeepBraces = true, IfStmtKind *IfKind = nullptr,
-                          bool UnindentWhitesmithsBraces = false);
+                          bool UnindentWhitesmithsBraces = false,
+                          bool IndentAfterExplicitAccessModifier = false);
   void parseChildBlock();
   void parsePPDirective();
   void parsePPDefine();
diff --git a/clang/unittests/Format/ConfigParseTest.cpp b/clang/unittests/Format/ConfigParseTest.cpp
index 86511edb9d40d..ccbc346901460 100644
--- a/clang/unittests/Format/ConfigParseTest.cpp
+++ b/clang/unittests/Format/ConfigParseTest.cpp
@@ -190,6 +190,7 @@ TEST(ConfigParseTest, ParsesConfigurationBools) {
   CHECK_PARSE_BOOL_FIELD(DerivePointerAlignment, "DerivePointerBinding");
   CHECK_PARSE_BOOL(DisableFormat);
   CHECK_PARSE_BOOL(IndentAccessModifiers);
+  CHECK_PARSE_BOOL(IndentImplicitAccessModifiers);
   CHECK_PARSE_BOOL(IndentCaseBlocks);
   CHECK_PARSE_BOOL(IndentCaseLabels);
   CHECK_PARSE_BOOL(IndentExportBlock);
diff --git a/clang/unittests/Format/FormatTest.cpp b/clang/unittests/Format/FormatTest.cpp
index bb630da34d7d9..656a352ecb005 100644
--- a/clang/unittests/Format/FormatTest.cpp
+++ b/clang/unittests/Format/FormatTest.cpp
@@ -24920,6 +24920,59 @@ TEST_F(FormatTest, IndentAccessModifiers) {
                Style);
 }
 
+TEST_F(FormatTest, IndentImplicitAccessModifiers) {
+  FormatStyle Style = getLLVMStyle();
+  Style.IndentAccessModifiers = true;
+  Style.IndentImplicitAccessModifiers = false;
+  Style.IndentWidth = 4;
+  Style.EmptyLineBeforeAccessModifier = FormatStyle::ELBAMS_Never;
+  Style.BreakBeforeBraces = FormatStyle::BS_Allman;
+
+  verifyFormat("class Outer\n"
+               "{\n"
+               "    public:\n"
+               "        struct Inner\n"
+               "        {\n"
+               "            bool first;\n"
+               "            bool second;\n"
+               "        };\n"
+               "};",
+               Style);
+  verifyFormat("struct S\n"
+               "{\n"
+               "    int before;\n"
+               "    private:\n"
+               "        int after;\n"
+               "};",
+               Style);
+  verifyFormat("union U\n"
+               "{\n"
+               "    int first;\n"
+               "    class Inner\n"
+               "    {\n"
+               "        public:\n"
+               "            int member;\n"
+               "    };\n"
+               "    int last;\n"
+               "};",
+               Style);
+  verifyFormat("class QtObject\n"
+               "{\n"
+               "    signals:\n"
+               "        void changed();\n"
+               "};",
+               Style);
+
+  Style.BreakBeforeBraces = FormatStyle::BS_Whitesmiths;
+  verifyFormat("struct S\n"
+               "    {\n"
+               "    int before;\n"
+               "    public:\n"
+               "        int after;\n"
+               "    };",
+               Style);
+}
+
 TEST_F(FormatTest, LimitlessStringsAndComments) {
   auto Style = getLLVMStyleWithColumns(0);
   constexpr StringRef Code(
diff --git a/clang/unittests/Format/FormatTestJava.cpp b/clang/unittests/Format/FormatTestJava.cpp
index a11fce963d820..86e4b2fc361b2 100644
--- a/clang/unittests/Format/FormatTestJava.cpp
+++ b/clang/unittests/Format/FormatTestJava.cpp
@@ -28,6 +28,18 @@ class FormatTestJava : public test::FormatTestBase {
   }
 };
 
+TEST_F(FormatTestJava, IndentImplicitAccessModifiersDoesNotAffectJava) {
+  FormatStyle Style = getDefaultStyle();
+  Style.IndentWidth = 4;
+  Style.IndentAccessModifiers = true;
+  Style.IndentImplicitAccessModifiers = false;
+  verifyFormat("class C {\n"
+               "        int before;\n"
+               "        public int after;\n"
+               "}",
+               Style);
+}
+
 TEST_F(FormatTestJava, NoAlternativeOperatorNames) {
   verifyFormat("someObject.and();");
 }

``````````

</details>


https://github.com/llvm/llvm-project/pull/227704


More information about the cfe-commits mailing list