Skip to content

Conversation

@kishikawakatsumi
Copy link
Contributor

Fixes #1959

For an example, see the following code

assertionFailure("""
  First line

  Second line

  """)

There are blank lines in line 3 and 5.

The conditions for a blank line are that StringSegments is only a newline character with no trivia (line 3), or that StringSegments is a zero-length string and the trailing trivia is a newline (line 5).

However, this condition would also match empty StringSegments immediately after string interpolation in the following code. (This case is covered by testStringInterpolationAtStartOfMultiLineStringLiteral.)

assertionFailure("""
  ABC
  \(myVar)
  """)

Therefore, I also added the condition that the previous token must be ended by a newline.

Fixes swiftlang#1959

For an example, see the following code

```swift
assertionFailure("""
  First line

  Second line

  """)
```

There are blank lines in line 3 and 5.

The conditions for a blank line are that StringSegments is only a newline character and has no trivia, or that StringSegments is a zero length string and the trailing trivia is a newline.

However, this condition would also match empty StringSegments immediately after string interpolation in the following code. (This case is covered by [`testStringInterpolationAtStartOfMultiLineStringLiteral`](https://github.com/apple/swift-syntax/blob/c2f6b9d18ed8c49b6e515b5e1f34b0fb5594eec7/Tests/SwiftBasicFormatTest/BasicFormatTests.swift#L211-L234).)

```swift
assertionFailure("""
  ABC
  \(myVar)
  """)
```

Therefore, I also added the condition that the previous token must be ended by a newline.
Copy link
Contributor

@Matejkob Matejkob left a comment

Choose a reason for hiding this comment

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

I've been reviewing your PR to learn more about the swift-syntax project, and I've come across a few points of interest. I've left a few in-line comments with some minor suggestions. 💙

return token
}

if token.text == "\n" && token.leadingTrivia.isEmpty && token.trailingTrivia.isEmpty {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm wondering whether it would be more beneficial to use isNewline instead of "\n". What do you think about this?

This can be achieved in a variety of ways. For example, we could use text.allSatisfy(\.isNewline). However, if we are prioritizing performance, we could aim for O(1) complexity with an approach like the following:

extension String {
  func isNewline() -> Bool {
    (first?.isNewline ?? false) && dropFirst().isEmpty
  }
}

Copy link
Member

Choose a reason for hiding this comment

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

I just wanted to say thanks for your review, @Matejkob. Those are valuable comments, they only got overridden by the fact that I found bigger design-ish improvements in the PR.

Comment on lines 514 to 520
if token.text.isEmpty
&& token.leadingTrivia.isEmpty
&& token.trailingTrivia.startsWithNewline
&& token.trailingTrivia.endsWithNewline
&& previousTokenWillEndWithNewline {
return token
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I noticed some minor formatting differences in this code compared to other code in the repo. Would you mind runing swift-format?

Copy link
Member

@ahoppen ahoppen left a comment

Choose a reason for hiding this comment

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

Thanks for the PR @kishikawakatsumi.

I have just investigated and it looks like the root causes of the issue are that

  1. We are setting an anchorPoint for empty lines. Anchor points are meant to be starting points at which the user didn’t provide any manual indentation relative to which we should format the rest of the tree. But for empty lines it’s not like the user didn’t provide any indentation. There simply wasn’t anything to indent.
  2. leadingTrivia.trimmingTrailingWhitespaceBeforeNewline was not considering whether the token text itself contained a newline in isBeforeNewline.

To fix these root causes, what I would suggest is to

  • Add a new variable behind combinedTrailingTrivia
/// Whether the leading trivia of the token is folloed by a newline.
let leadingTriviaIsFollowedByNewline: Bool = {
  if token.text.isEmpty && token.trailingTrivia.startsWithNewline {
    return true
  } else if token.text.first?.isNewline ?? false {
    return true
  } else if token.text.isEmpty && token.trailingTrivia.isEmpty && nextTokenWillStartWithNewline {
    return true
  } else {
    return false
  }
}()
  • Change the anchor point computation to the following
let isEmptyLine = token.leadingTrivia.isEmpty && leadingTriviaIsFollowedByNewline
if leadingTrivia.indentation(isOnNewline: isInitialToken || previousTokenWillEndWithNewline) == [] && !isEmptyLine {
  // If the token starts on a new line and does not have indentation, this
  // is the last non-indented token. Store its indentation level.
  // Do not
  anchorPoints[token] = currentIndentationLevel
}
  • And finally, also pass leadingTriviaIsFollowedByNewline to trimmingTrailingWhitepsaceBeforeNewline
leadingTrivia = leadingTrivia.trimmingTrailingWhitespaceBeforeNewline(isBeforeNewline: leadingTriviaIsFollowedByNewline)

…ing spaces from leadingTrivia, stop there if the token itself is a newline.
@kishikawakatsumi
Copy link
Contributor Author

@ahoppen Thanks for the guide. The formatting problem for blank lines in multi-line string literals has been fixed. However, it seems to affect other test cases. The following three test-cases fail.

  • DeclarationTests.testProtocolParsing()
  • EnumTests.testEnum21a()
  • OperatorDeclTests.testOperatorDecl15()

The diff for each is as follows. Perhaps a space that should be inserted has not been inserted?

// DeclarationTests.testProtocolParsing()
–protocol P{{}case <#identifier#>
+protocol P{{}case<#identifier#>
 }
// EnumTests.testEnum21a()
 enum Recovery1 {
–  case <#identifier#>:
+  case<#identifier#>:
 }
// OperatorDeclTests.testOperatorDecl15()
 precedencegroup E {
–  higherThan: <#identifier#>
+  higherThan:<#identifier#>
 }

@ahoppen
Copy link
Member

ahoppen commented Jul 29, 2023

Hi @kishikawakatsumi,

Oh, I should have totally checked whether my changes still formatted all the remaining diagnostics correctly, I’m sorry. The issue is that in the parsed tree, we represent the missing identifiers by identifiers with empty text as well and they only get transformed to the <#identifier#> placeholders in a later PresentMaker pass. Thus, all my rules of checking whether the text is empty also applied to these identifiers, which was causing the issues.

Looking into it further, I realized that the proper fix required some more, slightly more invasive changes that I, myself, would like to get a review on. I have opened #1964 with these changes and hope you don’t mind me closing this PR in favor of that one.

@ahoppen ahoppen closed this Jul 29, 2023
@kishikawakatsumi
Copy link
Contributor Author

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BasicFormat outputs incomplete indentation when formatting multiline string literals containing blank lines

3 participants