Multi-line string literals

Proposal doesn't mention that, but it seems \n is used as line terminator for multi-line string literals on any platform. On Windows it can cause some problems. For instance, if some package has a test which reads some text file, then compares result to a multi-line string literal it will fail. This occurs because git by default converts all new lines in all text files in the working copy to platform-specific ones.

Is it by design or is it oversight?

Proposal: swift-evolution/0168-multi-line-string-literals.md at main · apple/swift-evolution · GitHub

1 Like

During the review, there was a pull request which included these details, but it wasn't merged by the Core Team. Normalization of line breaks was also mentioned in the decision notes, and in the language reference book.

  • Decision Notes: Rationale

    • The quoted string should normalize newlines to \n in the value of the literal, regardless of whether the source file uses \n (Unix), \r\n (Windows), or \r (classic Mac) line endings. Likewise, when the compiler strips the initial and final newline from the literal value, it will strip one of any of the \n, \r\n, or \r line-ending sequences from both ends of the literal.
  • The Swift Programming Language: Lexical Structure

    Line breaks in a multiline string literal are normalized to use the line feed character. Even if your source file has a mix of carriage returns and line feeds, all of the line breaks in the string will be the same.

Multiline string literals currently require a line break immediately after the opening quotation marks. We could support line break options in this position, e.g. """LF by default, """CRLF when required by a data format or platform.

2 Likes

Do you mean the compiler should check what kind of a new line follows """ then use it in a literal?

No, my suggestion is to have something visible, like a keyword or attribute.

Unfortunately that means some code (in my case tests) can't be cross-platform. My current solution is:

extension String {
  var withPlatformNewLine: String {
    #if os(Windows)
      return replacingOccurrences(of: "\n", with: "\r\n")
    #else
      return self
    #endif
  }
}

If it's only for testing, you could split each string into an array of substrings (separated by any newline).

For example:

func XCTAssertEqualLines(
  _ lhs: String,
  _ rhs: String,
  _ message: @autoclosure () -> String = "",
  file: StaticString = #file,
  line: UInt = #line
) {
  let lhs: [Substring] = lhs.split(
    omittingEmptySubsequences: false,
    whereSeparator: \.isNewline
  )
  let rhs: [Substring] = rhs.split(
    omittingEmptySubsequences: false,
    whereSeparator: \.isNewline
  )
  XCTAssertEqual(lhs, rhs, message(), file: file, line: line)
}

In the future, we might be able to XCTAssertEqual(lhs.lines, rhs.lines) instead.

1 Like

This might be better for testing:

extension StringProtocol {
  internal var lines: [SubSequence] {
    split(omittingEmptySubsequences: false, whereSeparator: \.isNewline)
  }
}

The StringProtocol extension makes .lines available on String and Substring.

2 Likes

That sort of thing the correct solution. If Swift were to interpret source differently on different platforms, cross‐compilation would end up irreparably broken.

2 Likes

Much like this one. Does such change need to go through Swift Evolution?

Of course

This should be part of the standard library. Seems very useful.