diff --git a/Sources/SwiftBasicFormat/BasicFormat.swift b/Sources/SwiftBasicFormat/BasicFormat.swift index 43ad368d5d0..40a5948c55f 100644 --- a/Sources/SwiftBasicFormat/BasicFormat.swift +++ b/Sources/SwiftBasicFormat/BasicFormat.swift @@ -143,35 +143,50 @@ open class BasicFormat: SyntaxRewriter { return node.requiresIndent } - /// Whether a leading newline on `token` should be added. - open func requiresLeadingNewline(_ token: TokenSyntax) -> Bool { - // We don't want to add newlines inside string interpolation - if isInsideStringInterpolation(token) { + open func requiresNewline(between first: TokenSyntax?, and second: TokenSyntax?) -> Bool { + // We don't want to add newlines inside string interpolation. + // When first or second `TokenSyntax` is a multiline quote we want special handling + // even if it's inside a string interpolation, because it still requires newline + // after open quote and before close quote. + if let first, + isInsideStringInterpolation(first), + first.tokenKind != .multilineStringQuote, + second?.tokenKind != .multilineStringQuote + { return false - } - - if token.requiresLeadingNewline { - return true - } - - var ancestor: Syntax = Syntax(token) - while let parent = ancestor.parent { - ancestor = parent - if ancestor.position != token.position || ancestor.firstToken(viewMode: viewMode) != token { - break - } - if let ancestorsParent = ancestor.parent, childrenSeparatedByNewline(ancestorsParent) { + } else if let second { + if second.requiresLeadingNewline { return true } - switch ancestor.keyPathInParent { - case \IfConfigClauseSyntax.elements: - return true - default: - break + + var ancestor: Syntax = Syntax(second) + while let parent = ancestor.parent { + ancestor = parent + if ancestor.position != second.position || ancestor.firstToken(viewMode: viewMode) != second { + break + } + if let ancestorsParent = ancestor.parent, childrenSeparatedByNewline(ancestorsParent) { + return true + } + switch ancestor.keyPathInParent { + case \IfConfigClauseSyntax.elements: + return true + default: + break + } } } - return false + switch (first?.tokenKind, second?.tokenKind) { + case (.multilineStringQuote, .backslash), // string interpolation segment inside a multi-line string literal + (.multilineStringQuote, .multilineStringQuote), // empty multi-line string literal + (.multilineStringQuote, .stringSegment), // segment starting a multi-line string literal + (.stringSegment, .multilineStringQuote), // ending a multi-line string literal that has a string interpolation segment at its end + (.rightParen, .multilineStringQuote): // ending a multi-line string literal that has a string interpolation segment at its end + return true + default: + return false + } } open func requiresWhitespace(between first: TokenSyntax?, and second: TokenSyntax?) -> Bool { @@ -276,6 +291,12 @@ open class BasicFormat: SyntaxRewriter { let previousToken = self.previousToken ?? token.previousToken(viewMode: viewMode) let nextToken = token.nextToken(viewMode: viewMode) + /// In addition to existing trivia of `previousToken`, also considers + /// `previousToken` as ending with whitespace if it and `token` should be + /// separated by whitespace. + /// It does not consider whether a newline should be added between + /// `previousToken` and the `token` because that newline should be added to + /// the next token's trailing trivia. lazy var previousTokenWillEndWithWhitespace: Bool = { guard let previousToken = previousToken else { return false @@ -284,6 +305,8 @@ open class BasicFormat: SyntaxRewriter { || (requiresWhitespace(between: previousToken, and: token) && isMutable(previousToken)) }() + /// This method does not consider any posssible mutations to `previousToken` + /// because newlines should be added to the next token's leading trivia. lazy var previousTokenWillEndWithNewline: Bool = { guard let previousToken = previousToken else { // Assume that the start of the tree is equivalent to a newline so we @@ -293,10 +316,7 @@ open class BasicFormat: SyntaxRewriter { if previousToken.trailingTrivia.endsWithNewline { return true } - if case .stringSegment(let segment) = previousToken.tokenKind, segment.last?.isNewline ?? false { - return true - } - return false + return previousToken.isStringSegmentWithLastCharacterBeingNewline }() lazy var previousTokenIsStringLiteralEndingInNewline: Bool = { @@ -305,26 +325,29 @@ open class BasicFormat: SyntaxRewriter { // don't add a leading newline to the file. return true } - if case .stringSegment(let segment) = previousToken.tokenKind, segment.last?.isNewline ?? false { - return true - } - return false + return previousToken.isStringSegmentWithLastCharacterBeingNewline }() + /// Also considers `nextToken` as starting with a whitespace if a newline + /// should be added to it. It does not check whether `token` and `nextToken` + /// should be separated by whitespace because the whitespace should be added + /// to the `token`’s leading trivia. lazy var nextTokenWillStartWithWhitespace: Bool = { guard let nextToken = nextToken else { return false } return nextToken.leadingTrivia.startsWithWhitespace - || (requiresLeadingNewline(nextToken) && isMutable(nextToken)) + || (requiresNewline(between: token, and: nextToken) && isMutable(nextToken)) }() + /// Also considers `nextToken` as starting with a leading newline if `token` + /// and `nextToken` should be separated by a newline. lazy var nextTokenWillStartWithNewline: Bool = { guard let nextToken = nextToken else { return false } return nextToken.leadingTrivia.startsWithNewline - || (requiresLeadingNewline(nextToken) && isMutable(nextToken)) + || (requiresNewline(between: token, and: nextToken) && isMutable(nextToken) && !token.trailingTrivia.endsWithNewline && !token.isStringSegmentWithLastCharacterBeingNewline) }() /// This token's trailing trivia + any spaces or tabs at the start of the @@ -337,7 +360,7 @@ open class BasicFormat: SyntaxRewriter { var leadingTrivia = token.leadingTrivia var trailingTrivia = token.trailingTrivia - if requiresLeadingNewline(token) { + if requiresNewline(between: previousToken, and: token) { // Add a leading newline if the token requires it unless // - it already starts with a newline or // - the previous token ends with a newline @@ -402,3 +425,14 @@ open class BasicFormat: SyntaxRewriter { return token.detach().with(\.leadingTrivia, leadingTrivia).with(\.trailingTrivia, trailingTrivia) } } + +fileprivate extension TokenSyntax { + var isStringSegmentWithLastCharacterBeingNewline: Bool { + switch self.tokenKind { + case .stringSegment(let segment): + return segment.last?.isNewline ?? false + default: + return false + } + } +} diff --git a/Sources/SwiftParserDiagnostics/DiagnosticExtensions.swift b/Sources/SwiftParserDiagnostics/DiagnosticExtensions.swift index 9ce0838f598..1ae6c18c627 100644 --- a/Sources/SwiftParserDiagnostics/DiagnosticExtensions.swift +++ b/Sources/SwiftParserDiagnostics/DiagnosticExtensions.swift @@ -167,11 +167,12 @@ extension FixIt.MultiNodeChange { } } - if let previousToken = node.previousToken(viewMode: .all), + if let previousToken = node.previousToken(viewMode: .fixedUp), previousToken.presence == .present, let firstToken = node.firstToken(viewMode: .all), previousToken.trailingTrivia.allSatisfy({ $0.isWhitespace }), - !BasicFormat().requiresWhitespace(between: previousToken, and: firstToken) + !BasicFormat().requiresWhitespace(between: previousToken, and: firstToken), + !BasicFormat().requiresNewline(between: previousToken, and: firstToken) { // If the previous token and this node don't need to be separated, remove // the separation. diff --git a/Sources/SwiftParserDiagnostics/MissingNodesError.swift b/Sources/SwiftParserDiagnostics/MissingNodesError.swift index 29137d69fde..b26930eb60e 100644 --- a/Sources/SwiftParserDiagnostics/MissingNodesError.swift +++ b/Sources/SwiftParserDiagnostics/MissingNodesError.swift @@ -22,7 +22,7 @@ fileprivate func findCommonAncestor(_ nodes: [Syntax]) -> Syntax? { } class NoNewlinesFormat: BasicFormat { - override func requiresLeadingNewline(_ token: TokenSyntax) -> Bool { + override func requiresNewline(between first: TokenSyntax?, and second: TokenSyntax?) -> Bool { return false } } diff --git a/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift b/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift index 33ea874aeb5..8236df6723d 100644 --- a/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift +++ b/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift @@ -1066,12 +1066,10 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { } if case .stringSegment(let segment) = node.segments.last { if let invalidContent = segment.unexpectedBeforeContent?.onlyToken(where: { $0.trailingTrivia.contains(where: { $0.isBackslash }) }) { - let leadingTrivia = segment.content.leadingTrivia - let trailingTrivia = segment.content.trailingTrivia let fixIt = FixIt( message: .removeBackslash, changes: [ - .makePresent(segment.content, leadingTrivia: leadingTrivia, trailingTrivia: trailingTrivia), + .makePresent(segment.content), .makeMissing(invalidContent, transferTrivia: false), ] ) diff --git a/Tests/SwiftParserTest/ExpressionTests.swift b/Tests/SwiftParserTest/ExpressionTests.swift index d7ef84e2a79..bd45d0e2d1c 100644 --- a/Tests/SwiftParserTest/ExpressionTests.swift +++ b/Tests/SwiftParserTest/ExpressionTests.swift @@ -550,16 +550,28 @@ final class ExpressionTests: XCTestCase { NoteSpec(message: #"to match this opening '"""'"#) ] ) - ] + ], + fixedSource: ##""" + """" + """ + """## ) assertParse( ##""" - """""1️⃣ + ℹ️"""""1️⃣ """##, diagnostics: [ - DiagnosticSpec(message: #"expected '"""' to end string literal"#) - ] + DiagnosticSpec( + message: #"expected '"""' to end string literal"#, + notes: [NoteSpec(message: #"to match this opening '"""'"#)], + fixIts: [#"insert '"""'"#] + ) + ], + fixedSource: ##""" + """"" + """ + """## ) assertParse( @@ -589,8 +601,12 @@ final class ExpressionTests: XCTestCase { #"""1️⃣ """##, diagnostics: [ - DiagnosticSpec(message: ##"expected '"""#' to end string literal"##) - ] + DiagnosticSpec(message: ##"expected '"""#' to end string literal"##, fixIts: [##"insert '"""#'"##]) + ], + fixedSource: ##""" + #""" + """# + """## ) assertParse( @@ -598,8 +614,12 @@ final class ExpressionTests: XCTestCase { #"""a1️⃣ """##, diagnostics: [ - DiagnosticSpec(message: ##"expected '"""#' to end string literal"##) - ] + DiagnosticSpec(message: ##"expected '"""#' to end string literal"##, fixIts: [##"insert '"""#'"##]) + ], + fixedSource: ##""" + #"""a + """# + """## ) assertParse( diff --git a/Tests/SwiftParserTest/translated/AvailabilityQueryTests.swift b/Tests/SwiftParserTest/translated/AvailabilityQueryTests.swift index c2b811ef723..48e5e43b8a1 100644 --- a/Tests/SwiftParserTest/translated/AvailabilityQueryTests.swift +++ b/Tests/SwiftParserTest/translated/AvailabilityQueryTests.swift @@ -96,7 +96,7 @@ final class AvailabilityQueryTests: XCTestCase { DiagnosticSpec(message: "expected ',' joining parts of a multi-clause condition", fixIts: ["replace '&&' with ','"]) ], fixedSource: """ - if #available(OSX 10.51, *) , #available(OSX 10.52, *) { + if #available(OSX 10.51, *), #available(OSX 10.52, *) { } """ ) @@ -385,7 +385,7 @@ final class AvailabilityQueryTests: XCTestCase { DiagnosticSpec(message: "expected ',' joining platforms in availability condition", fixIts: ["replace '||' with ','"]) ], fixedSource: """ - if #available(OSX 10.51 , iOS 8.0) { + if #available(OSX 10.51, iOS 8.0) { } """ ) diff --git a/Tests/SwiftParserTest/translated/AvailabilityQueryUnavailabilityTests.swift b/Tests/SwiftParserTest/translated/AvailabilityQueryUnavailabilityTests.swift index 75387992907..c56862f9f7c 100644 --- a/Tests/SwiftParserTest/translated/AvailabilityQueryUnavailabilityTests.swift +++ b/Tests/SwiftParserTest/translated/AvailabilityQueryUnavailabilityTests.swift @@ -78,8 +78,15 @@ final class AvailabilityQueryUnavailabilityTests: XCTestCase { } """, diagnostics: [ - DiagnosticSpec(message: "expected ',' joining parts of a multi-clause condition", fixIts: ["replace '&&' with ','"]) - ] + DiagnosticSpec( + message: "expected ',' joining parts of a multi-clause condition", + fixIts: ["replace '&&' with ','"] + ) + ], + fixedSource: """ + if #unavailable(OSX 10.51), #unavailable(OSX 10.52) { + } + """ ) } @@ -367,8 +374,15 @@ final class AvailabilityQueryUnavailabilityTests: XCTestCase { } """, diagnostics: [ - DiagnosticSpec(message: "expected ',' joining platforms in availability condition") - ] + DiagnosticSpec( + message: "expected ',' joining platforms in availability condition", + fixIts: ["replace '||' with ','"] + ) + ], + fixedSource: """ + if #unavailable(OSX 10.51, iOS 8.0) { + } + """ ) } diff --git a/Tests/SwiftParserTest/translated/MultilineErrorsTests.swift b/Tests/SwiftParserTest/translated/MultilineErrorsTests.swift index adb96da9949..724145ca23e 100644 --- a/Tests/SwiftParserTest/translated/MultilineErrorsTests.swift +++ b/Tests/SwiftParserTest/translated/MultilineErrorsTests.swift @@ -582,13 +582,26 @@ final class MultilineErrorsTests: XCTestCase { func testMultilineErrors26() { assertParseWithAllNewlineEndings( ##""" - _ = """ + _ = ℹ️""" foo1️⃣\2️⃣ """##, diagnostics: [ - DiagnosticSpec(locationMarker: "1️⃣", message: "invalid escape sequence in literal"), - DiagnosticSpec(locationMarker: "2️⃣", message: #"expected '"""' to end string literal"#), - ] + DiagnosticSpec( + locationMarker: "1️⃣", + message: "invalid escape sequence in literal" + ), + DiagnosticSpec( + locationMarker: "2️⃣", + message: #"expected '"""' to end string literal"#, + notes: [NoteSpec(message: #"to match this opening '"""'"#)], + fixIts: [#"insert '"""'"#] + ), + ], + fixedSource: ##""" + _ = """ + foo\ + """ + """## ) } @@ -623,6 +636,7 @@ final class MultilineErrorsTests: XCTestCase { fixedSource: #""" _ = """ \#u{20} + """ """# ) diff --git a/Tests/SwiftParserTest/translated/MultilinePoundDiagnosticArgRdar41154797Tests.swift b/Tests/SwiftParserTest/translated/MultilinePoundDiagnosticArgRdar41154797Tests.swift index ee0a506abe2..d819fe38459 100644 --- a/Tests/SwiftParserTest/translated/MultilinePoundDiagnosticArgRdar41154797Tests.swift +++ b/Tests/SwiftParserTest/translated/MultilinePoundDiagnosticArgRdar41154797Tests.swift @@ -18,12 +18,26 @@ final class MultilinePoundDiagnosticArgRdar41154797Tests: XCTestCase { func testMultilinePoundDiagnosticArgRdar411547971() { assertParse( ##""" - #error("""1️⃣ + #error1️⃣(2️⃣"""3️⃣ """##, diagnostics: [ - DiagnosticSpec(message: #"expected '"""' to end string literal"#), - DiagnosticSpec(message: "expected ')' to end macro expansion"), - ] + DiagnosticSpec( + locationMarker: "3️⃣", + message: #"expected '"""' to end string literal"#, + notes: [NoteSpec(locationMarker: "2️⃣", message: #"to match this opening '"""'"#)], + fixIts: [#"insert '"""'"#] + ), + DiagnosticSpec( + locationMarker: "3️⃣", + message: "expected ')' to end macro expansion", + notes: [NoteSpec(locationMarker: "1️⃣", message: "to match this opening '('")], + fixIts: ["insert ')'"] + ), + ], + fixedSource: ##""" + #error(""" + """) + """## ) } diff --git a/Tests/SwiftParserTest/translated/MultilineStringTests.swift b/Tests/SwiftParserTest/translated/MultilineStringTests.swift index 0ca78fdf1dc..263ed297fa0 100644 --- a/Tests/SwiftParserTest/translated/MultilineStringTests.swift +++ b/Tests/SwiftParserTest/translated/MultilineStringTests.swift @@ -411,6 +411,82 @@ final class MultilineStringTests: XCTestCase { ) } + func testMultilineString47() { + assertParse( + #""" + _ = """1️⃣""" + """#, + diagnostics: [ + DiagnosticSpec( + message: "multi-line string literal closing delimiter must begin on a new line", + fixIts: ["insert newline"] + ) + ], + fixedSource: #""" + _ = """ + """ + """# + ) + } + + func testMultilineString48() { + assertParse( + #""" + _ = """A1️⃣""" + """#, + diagnostics: [ + DiagnosticSpec( + message: "multi-line string literal closing delimiter must begin on a new line", + fixIts: ["insert newline"] + ) + ], + fixedSource: #""" + _ = """A + """ + """# + ) + } + + func testMultilineString49() { + assertParse( + #""" + _ = ℹ️"""1️⃣ + """#, + diagnostics: [ + DiagnosticSpec( + message: #"expected '"""' to end string literal"#, + notes: [NoteSpec(message: #"to match this opening '"""'"#)], + fixIts: [#"insert '"""'"#] + ) + ], + fixedSource: #""" + _ = """ + """ + """# + ) + } + + func testMultilineString50() { + assertParse( + #""" + _ = ℹ️""" + A1️⃣ + """#, + diagnostics: [ + DiagnosticSpec( + message: #"expected '"""' to end string literal"#, + notes: [NoteSpec(message: #"to match this opening '"""'"#)], + fixIts: [#"insert '"""'"#] + ) + ], + fixedSource: #""" + _ = """ + A + """ + """# + ) + } + func testEscapeNewlineInRawString() { assertParse( ##""" diff --git a/Tests/SwiftParserTest/translated/StringLiteralEofTests.swift b/Tests/SwiftParserTest/translated/StringLiteralEofTests.swift index 45b4e5bb6c2..280d85de705 100644 --- a/Tests/SwiftParserTest/translated/StringLiteralEofTests.swift +++ b/Tests/SwiftParserTest/translated/StringLiteralEofTests.swift @@ -74,12 +74,22 @@ final class StringLiteralEofTests: XCTestCase { assertParse( #""" // NOTE: DO NOT add a newline at EOF. - _ = """ + _ = ℹ️""" foo1️⃣ """#, diagnostics: [ - DiagnosticSpec(message: #"expected '"""' to end string literal"#) - ] + DiagnosticSpec( + message: #"expected '"""' to end string literal"#, + notes: [NoteSpec(message: #"to match this opening '"""'"#)], + fixIts: [#"insert '"""'"#] + ) + ], + fixedSource: #""" + // NOTE: DO NOT add a newline at EOF. + _ = """ + foo + """ + """# ) } @@ -98,7 +108,8 @@ final class StringLiteralEofTests: XCTestCase { fixedSource: ##""" _ = """ foo - \(<#expression#>)""" + \(<#expression#>) + """ """## // FIXME: The closing delimiter should be put on the new line ) @@ -108,31 +119,76 @@ final class StringLiteralEofTests: XCTestCase { // NOTE: DO NOT add a newline at EOF. assertParse( ##""" - _ = """ + _ = 1️⃣""" foo - \("bar1️⃣ + \2️⃣(3️⃣"bar4️⃣ """##, diagnostics: [ - DiagnosticSpec(message: #"expected '"' to end string literal"#), - DiagnosticSpec(message: "expected ')' in string literal"), - DiagnosticSpec(message: #"expected '"""' to end string literal"#), - ] + DiagnosticSpec( + locationMarker: "4️⃣", + message: #"expected '"' to end string literal"#, + notes: [NoteSpec(locationMarker: "3️⃣", message: #"to match this opening '"'"#)], + fixIts: [#"insert '"'"#] + ), + DiagnosticSpec( + locationMarker: "4️⃣", + message: "expected ')' in string literal", + notes: [NoteSpec(locationMarker: "2️⃣", message: "to match this opening '('")], + fixIts: ["insert ')'"] + ), + DiagnosticSpec( + locationMarker: "4️⃣", + message: #"expected '"""' to end string literal"#, + notes: [NoteSpec(locationMarker: "1️⃣", message: #"to match this opening '"""'"#)], + fixIts: [#"insert '"""'"#] + ), + ], + fixedSource: ##""" + _ = """ + foo + \("bar") + """ + """## ) } func testStringLiteralEof8() { assertParse( ##""" - _ = """ - \("bar1️⃣ - 2️⃣baz3️⃣ + _ = 1️⃣""" + \2️⃣(3️⃣"bar4️⃣ + 5️⃣baz6️⃣ """##, diagnostics: [ - DiagnosticSpec(locationMarker: "1️⃣", message: #"expected '"' to end string literal"#), - DiagnosticSpec(locationMarker: "2️⃣", message: "unexpected code 'baz' in string literal"), - DiagnosticSpec(locationMarker: "3️⃣", message: "expected ')' in string literal"), - DiagnosticSpec(locationMarker: "3️⃣", message: #"expected '"""' to end string literal"#), - ] + DiagnosticSpec( + locationMarker: "4️⃣", + message: #"expected '"' to end string literal"#, + notes: [NoteSpec(locationMarker: "3️⃣", message: #"to match this opening '"'"#)], + fixIts: [#"insert '"'"#] + ), + DiagnosticSpec( + locationMarker: "5️⃣", + message: "unexpected code 'baz' in string literal" + ), + DiagnosticSpec( + locationMarker: "6️⃣", + message: "expected ')' in string literal", + notes: [NoteSpec(locationMarker: "2️⃣", message: "to match this opening '('")], + fixIts: ["insert ')'"] + ), + DiagnosticSpec( + locationMarker: "6️⃣", + message: #"expected '"""' to end string literal"#, + notes: [NoteSpec(locationMarker: "1️⃣", message: #"to match this opening '"""'"#)], + fixIts: [#"insert '"""'"#] + ), + ], + fixedSource: ##""" + _ = """ + \("bar" + baz) + """ + """## ) } diff --git a/Tests/SwiftParserTest/translated/UnclosedStringInterpolationTests.swift b/Tests/SwiftParserTest/translated/UnclosedStringInterpolationTests.swift index 6defd396454..61e9b79c880 100644 --- a/Tests/SwiftParserTest/translated/UnclosedStringInterpolationTests.swift +++ b/Tests/SwiftParserTest/translated/UnclosedStringInterpolationTests.swift @@ -96,15 +96,37 @@ final class UnclosedStringInterpolationTests: XCTestCase { func testUnclosedStringInterpolation8() { assertParse( ##""" - _ = """ - \( - """1️⃣ + _ = 1️⃣""" + \2️⃣( + 3️⃣"""4️⃣ """##, diagnostics: [ - DiagnosticSpec(message: #"expected '"""' to end string literal"#), - DiagnosticSpec(message: "expected ')' in string literal"), - DiagnosticSpec(message: #"expected '"""' to end string literal"#), - ] + DiagnosticSpec( + locationMarker: "4️⃣", + message: #"expected '"""' to end string literal"#, + notes: [NoteSpec(locationMarker: "3️⃣", message: #"to match this opening '"""'"#)], + fixIts: [#"insert '"""'"#] + ), + DiagnosticSpec( + locationMarker: "4️⃣", + message: "expected ')' in string literal", + notes: [NoteSpec(locationMarker: "2️⃣", message: "to match this opening '('")], + fixIts: ["insert ')'"] + ), + DiagnosticSpec( + locationMarker: "4️⃣", + message: #"expected '"""' to end string literal"#, + notes: [NoteSpec(locationMarker: "1️⃣", message: #"to match this opening '"""'"#)], + fixIts: [#"insert '"""'"#] + ), + ], + fixedSource: ##""" + _ = """ + \( + """ + """) + """ + """## ) } diff --git a/Tests/SwiftSyntaxBuilderTest/StringLiteralTests.swift b/Tests/SwiftSyntaxBuilderTest/StringLiteralExprSyntaxTests.swift similarity index 89% rename from Tests/SwiftSyntaxBuilderTest/StringLiteralTests.swift rename to Tests/SwiftSyntaxBuilderTest/StringLiteralExprSyntaxTests.swift index 1de0bf7a62b..b760e2dd309 100644 --- a/Tests/SwiftSyntaxBuilderTest/StringLiteralTests.swift +++ b/Tests/SwiftSyntaxBuilderTest/StringLiteralExprSyntaxTests.swift @@ -14,7 +14,7 @@ import XCTest import SwiftSyntax import SwiftSyntaxBuilder -final class StringLiteralTests: XCTestCase { +final class StringLiteralExprSyntaxTests: XCTestCase { func testStringLiteral() { let leadingTrivia = Trivia.unexpectedText("␣") let testCases: [UInt: (String, String)] = [ @@ -334,4 +334,44 @@ final class StringLiteralTests: XCTestCase { """# ) } + + func testMultiStringOpeningQuote() { + assertBuildResult( + StringLiteralExprSyntax(openQuote: .multilineStringQuoteToken(), content: "a", closeQuote: .multilineStringQuoteToken()), + #""" + """ + a + """ + """# + ) + + assertBuildResult( + StringLiteralExprSyntax( + openQuote: .multilineStringQuoteToken(), + segments: StringLiteralSegmentsSyntax { + .expressionSegment( + ExpressionSegmentSyntax( + expressions: TupleExprElementListSyntax { + TupleExprElementSyntax( + expression: StringLiteralExprSyntax( + openQuote: .multilineStringQuoteToken(), + content: "a", + closeQuote: .multilineStringQuoteToken() + ) + ) + } + ) + ) + }, + closeQuote: .multilineStringQuoteToken() + ), + #""" + """ + \(""" + a + """) + """ + """# + ) + } }