Skip to content

[clang-format] Propagate LeadingEmptyLinesAffected when joining lines #146761

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 3 commits into
base: main
Choose a base branch
from

Conversation

tJener
Copy link
Contributor

@tJener tJener commented Jul 2, 2025

Before this commit, when LineJoiner joins a line with affected leading whitespace, it would drop the knowledge of this entirely. However, when the AffectedRangeManager is computing the affected lines, the leading empty whitespace lines are potentially considered for non-first tokens in the AnnotatedLine. This causes a discrepancy in behavior when an AnnotatedLine is put together from joining multiple lines versus when it is not.

We change LineJoiner::join to follow AffectedRangeManager's logic, considering the leading whitespace when determining Affected for a token if the previous token did not have children.

// Stores whether we need to look at the leading newlines of the next token
// in order to determine whether it was affected.
bool IncludeLeadingNewlines = false;
// Stores whether the first child line of any of this line's tokens is
// affected.
bool SomeFirstChildAffected = false;
assert(Line->First);
for (FormatToken *Tok = Line->First; Tok; Tok = Tok->Next) {
// Determine whether 'Tok' was affected.
if (affectsTokenRange(*Tok, *Tok, IncludeLeadingNewlines))
SomeTokenAffected = true;
// Determine whether the first child of 'Tok' was affected.
if (!Tok->Children.empty() && Tok->Children.front()->Affected)
SomeFirstChildAffected = true;
IncludeLeadingNewlines = Tok->Children.empty();
}

Fixes #138942.

Before this commit, when `LineJoiner` joins a line with affected
leading whitespace, it would drop the knowledge of this
entirely. However, when the `AffectedRangeManager` is computing the
affected lines, the leading empty whitespace lines are potentially
considered for non-first tokens in the `AnnotatedLine`. This causes a
discrepancy in behavior when an `AnnotatedLine` is put together from
joining multiple lines versus when it is not.

We change `LineJoiner::join` to follow `AffectedRangeManager`'s logic,
considering the leading whitespace when determining `Affected` for a
token if the previous token did not have children.

https://github.com/llvm/llvm-project/blob/a63f57262898588b576d66e5fd79c0aa64b35f2d/clang/lib/Format/AffectedRangeManager.cpp#L111-L130

Fixes llvm#138942.
@tJener tJener requested a review from owenca July 2, 2025 19:10
@llvmbot
Copy link
Member

llvmbot commented Jul 2, 2025

@llvm/pr-subscribers-clang-format

Author: Eric Li (tJener)

Changes

Before this commit, when LineJoiner joins a line with affected leading whitespace, it would drop the knowledge of this entirely. However, when the AffectedRangeManager is computing the affected lines, the leading empty whitespace lines are potentially considered for non-first tokens in the AnnotatedLine. This causes a discrepancy in behavior when an AnnotatedLine is put together from joining multiple lines versus when it is not.

We change LineJoiner::join to follow AffectedRangeManager's logic, considering the leading whitespace when determining Affected for a token if the previous token did not have children.

// Stores whether we need to look at the leading newlines of the next token
// in order to determine whether it was affected.
bool IncludeLeadingNewlines = false;
// Stores whether the first child line of any of this line's tokens is
// affected.
bool SomeFirstChildAffected = false;
assert(Line->First);
for (FormatToken *Tok = Line->First; Tok; Tok = Tok->Next) {
// Determine whether 'Tok' was affected.
if (affectsTokenRange(*Tok, *Tok, IncludeLeadingNewlines))
SomeTokenAffected = true;
// Determine whether the first child of 'Tok' was affected.
if (!Tok->Children.empty() && Tok->Children.front()->Affected)
SomeFirstChildAffected = true;
IncludeLeadingNewlines = Tok->Children.empty();
}

Fixes #138942.


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

2 Files Affected:

  • (modified) clang/lib/Format/UnwrappedLineFormatter.cpp (+2)
  • (modified) clang/unittests/Format/FormatTestSelective.cpp (+2)
diff --git a/clang/lib/Format/UnwrappedLineFormatter.cpp b/clang/lib/Format/UnwrappedLineFormatter.cpp
index f2ed027b2c047..d22bfc61ed736 100644
--- a/clang/lib/Format/UnwrappedLineFormatter.cpp
+++ b/clang/lib/Format/UnwrappedLineFormatter.cpp
@@ -988,6 +988,8 @@ class LineJoiner {
     assert(!B.First->Previous);
     if (B.Affected)
       A.Affected = true;
+    else if (B.LeadingEmptyLinesAffected && A.Last->Children.empty())
+      A.Affected = true;
     A.Last->Next = B.First;
     B.First->Previous = A.Last;
     B.First->CanBreakBefore = true;
diff --git a/clang/unittests/Format/FormatTestSelective.cpp b/clang/unittests/Format/FormatTestSelective.cpp
index 624684c7a079b..3500b7d47e99d 100644
--- a/clang/unittests/Format/FormatTestSelective.cpp
+++ b/clang/unittests/Format/FormatTestSelective.cpp
@@ -41,6 +41,8 @@ TEST_F(FormatTestSelective, RemovesTrailingWhitespaceOfFormattedLine) {
   EXPECT_EQ("int a;", format("int a;         ", 0, 0));
   EXPECT_EQ("int a;\n", format("int a;  \n   \n   \n ", 0, 0));
   EXPECT_EQ("int a;\nint b;    ", format("int a;  \nint b;    ", 0, 0));
+
+  EXPECT_EQ("void f() {}\n", format("void f() {\n  \n}\n", 11, 0));
 }
 
 TEST_F(FormatTestSelective, FormatsCorrectRegionForLeadingWhitespace) {

Copy link
Contributor

@HazardyKnusperkeks HazardyKnusperkeks left a comment

Choose a reason for hiding this comment

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

Please wait a few days, or until another approval.

@@ -986,7 +986,7 @@ class LineJoiner {
void join(AnnotatedLine &A, const AnnotatedLine &B) {
assert(!A.Last->Next);
assert(!B.First->Previous);
if (B.Affected)
if (B.Affected || (B.LeadingEmptyLinesAffected && A.Last->Children.empty()))
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a test case for A.Last->Children.empty() being false?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not familiar with when the current implementation would merge two lines where !A.Last->Children.empty(). Do you have any suggestions?

Co-authored-by: Owen Pan <owenpiano@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[clang-format] unexpected formatting on empty lines
4 participants