diff --git a/Sources/SwiftSyntax/Trivia.swift b/Sources/SwiftSyntax/Trivia.swift index 07e3925ee9e..b7f9778e6ac 100644 --- a/Sources/SwiftSyntax/Trivia.swift +++ b/Sources/SwiftSyntax/Trivia.swift @@ -42,6 +42,68 @@ public struct Trivia: Sendable { pieces.isEmpty } + /// The string contents of all the comment pieces with any comments tokens trimmed. + /// + /// Each element in the array is the trimmed contents of a line comment, or, in the case of a multi-line comment a trimmed, concatenated single string. + public var commentValues: [String] { + var comments = [String]() + var partialComments = [String]() + + var foundStartOfCodeBlock = false + var foundEndOfCodeBlock = false + var isInCodeBlock: Bool { foundStartOfCodeBlock && !foundEndOfCodeBlock } + + for piece in pieces { + switch piece { + case .blockComment(let text), .docBlockComment(let text): + let text = text.trimmingCharacters(in: "\n") + + foundStartOfCodeBlock = text.hasPrefix("/*") + foundEndOfCodeBlock = text.hasSuffix("*/") + + let sanitized = + text + .split(separator: "\n") + .map { $0.trimmingAnyCharacters(in: "/*").trimmingAnyCharacters(in: " ") } + .filter { !$0.isEmpty } + .joined(separator: " ") + + appendPartialCommentIfPossible(sanitized) + + case .lineComment(let text), .docLineComment(let text): + if isInCodeBlock { + appendPartialCommentIfPossible(text) + } else { + comments.append(String(text.trimmingPrefix("/ "))) + } + + default: + break + } + + if foundEndOfCodeBlock, !partialComments.isEmpty { + appendSubstringsToLines() + partialComments.removeAll() + } + } + + if !partialComments.isEmpty { + appendSubstringsToLines() + } + + func appendPartialCommentIfPossible(_ text: String) { + guard partialComments.isEmpty || !text.isEmpty else { return } + + partialComments.append(text) + } + + func appendSubstringsToLines() { + comments.append(partialComments.joined(separator: " ")) + } + + return comments + } + /// The length of all the pieces in this ``Trivia``. public var sourceLength: SourceLength { return pieces.map({ $0.sourceLength }).reduce(.zero, +) diff --git a/Sources/SwiftSyntax/Utils.swift b/Sources/SwiftSyntax/Utils.swift index b9ac1e85a7c..68777aa3bcc 100644 --- a/Sources/SwiftSyntax/Utils.swift +++ b/Sources/SwiftSyntax/Utils.swift @@ -120,3 +120,49 @@ extension RawUnexpectedNodesSyntax { self.init(raw: raw) } } + +extension String { + func trimmingCharacters(in charactersToTrim: any BidirectionalCollection) -> Substring { + // TODO: adammcarter - this feels a bit dirty + self[startIndex...].trimmingAnyCharacters(in: charactersToTrim) + } + + func trimmingPrefix(_ charactersToTrim: any BidirectionalCollection) -> Substring { + self[startIndex...].trimmingAnyCharactersFromPrefix(in: charactersToTrim) + } + + func trimmingSuffix(_ charactersToTrim: any BidirectionalCollection) -> Substring { + self[startIndex...].trimmingAnyCharactersFromSuffix(in: charactersToTrim) + } +} + +extension Substring { + func trimmingAnyCharacters(in charactersToTrim: any BidirectionalCollection) -> Substring { + trimmingAnyCharactersFromPrefix(in: charactersToTrim).trimmingAnyCharactersFromSuffix(in: charactersToTrim) + } + + func trimmingAnyCharactersFromPrefix(in charactersToTrim: any BidirectionalCollection) -> Self { + dropFirst(countOfSequentialCharacters(charactersToTrim, in: self)) + } + + func trimmingAnyCharactersFromSuffix(in charactersToTrim: any BidirectionalCollection) -> Self { + dropLast(countOfSequentialCharacters(charactersToTrim, in: reversed())) + } +} + +private func countOfSequentialCharacters( + _ charactersToCount: any BidirectionalCollection, + in characters: any BidirectionalCollection +) -> Int { + var count = 0 + + for character in characters { + if charactersToCount.contains(character) { + count += 1 + } else { + break + } + } + + return count +} diff --git a/Tests/SwiftSyntaxTest/TriviaTests.swift b/Tests/SwiftSyntaxTest/TriviaTests.swift index 6360d2f3ec5..329deee7692 100644 --- a/Tests/SwiftSyntaxTest/TriviaTests.swift +++ b/Tests/SwiftSyntaxTest/TriviaTests.swift @@ -69,4 +69,325 @@ class TriviaTests: XCTestCase { XCTAssertNotEqual(TriviaPiece.unexpectedText("e"), .unexpectedText("f")) XCTAssertNotEqual(TriviaPiece.unexpectedText("e"), .lineComment("e")) } + + func testTriviaCommentValues() { + XCTAssertTrue(Trivia(pieces: []).commentValues.isEmpty) + + // MARK: line comment + + XCTAssertEqual( + Trivia(pieces: [.lineComment("")]).commentValues, + [""] + ) + + XCTAssertEqual( + Trivia(pieces: [.lineComment("Some line comment")]).commentValues, + ["Some line comment"] + ) + + XCTAssertEqual( + Trivia(pieces: [.lineComment("// Some line comment")]).commentValues, + ["Some line comment"] + ) + + XCTAssertEqual( + Trivia(pieces: [ + .lineComment("// Some line comment"), + .lineComment("// Another"), + ]).commentValues, + [ + "Some line comment", + "Another", + ] + ) + + XCTAssertEqual( + Trivia(pieces: [ + .lineComment("// Some line comment"), + .lineComment("Other"), + ]).commentValues, + [ + "Some line comment", + "Other", + ] + ) + + // MARK: doc line comment + + XCTAssertEqual( + Trivia(pieces: [.docLineComment("")]).commentValues, + [""] + ) + + XCTAssertEqual( + Trivia(pieces: [.docLineComment("Some doc line comment")]).commentValues, + ["Some doc line comment"] + ) + + XCTAssertEqual( + Trivia(pieces: [.docLineComment("/// Some doc line comment")]).commentValues, + ["Some doc line comment"] + ) + + XCTAssertEqual( + Trivia(pieces: [ + .docLineComment("/// Some doc line comment"), + .docLineComment("/// Another"), + ]).commentValues, + [ + "Some doc line comment", + "Another", + ] + ) + + XCTAssertEqual( + Trivia(pieces: [ + .docLineComment("/// Some doc line comment"), + .docLineComment("Other"), + ]).commentValues, + [ + "Some doc line comment", + "Other", + ] + ) + + // MARK: block comment + + XCTAssertEqual( + Trivia(pieces: [.blockComment("")]).commentValues, + [""] + ) + + XCTAssertEqual( + Trivia(pieces: [.blockComment("Some block comment")]).commentValues, + ["Some block comment"] + ) + + XCTAssertEqual( + Trivia(pieces: [.blockComment("/* Some block comment */")]).commentValues, + ["Some block comment"] + ) + + XCTAssertEqual( + Trivia(pieces: [ + .blockComment("/* Some block comment"), + .blockComment("* spread on many lines */"), + ]).commentValues, + [ + "Some block comment spread on many lines" + ] + ) + + XCTAssertEqual( + Trivia(pieces: [ + .blockComment("/* Some block comment"), + .blockComment("* spread on many lines"), + .blockComment("*/"), + ]).commentValues, + [ + "Some block comment spread on many lines" + ] + ) + + XCTAssertEqual( + Trivia(pieces: [ + .blockComment("/* Some block comment"), + .blockComment("* spread on many lines */"), + .blockComment("/* Another block comment */"), + ]).commentValues, + [ + "Some block comment spread on many lines", + "Another block comment", + ] + ) + + XCTAssertEqual( + Trivia(pieces: [ + .blockComment("/* Some block comment"), + .blockComment("* spread on many lines */"), + .newlines(2), + .blockComment("/* Another block comment */"), + ]).commentValues, + [ + "Some block comment spread on many lines", + "Another block comment", + ] + ) + + XCTAssertEqual( + Trivia(pieces: [ + .blockComment( + """ + /* + Some block comment + spread on many lines + */ + """ + ) + ]).commentValues, + ["Some block comment spread on many lines"] + ) + + XCTAssertEqual( + Trivia(pieces: [ + .blockComment( + """ + /* + * Some block comment + * spread on many lines + */ + """ + ) + ]).commentValues, + ["Some block comment spread on many lines"] + ) + + // MARK: doc block comment + + XCTAssertEqual( + Trivia(pieces: [.docBlockComment("")]).commentValues, + [""] + ) + + XCTAssertEqual( + Trivia(pieces: [.docBlockComment("Some doc block comment")]).commentValues, + ["Some doc block comment"] + ) + + XCTAssertEqual( + Trivia(pieces: [.docBlockComment("/** Some doc block comment */")]).commentValues, + ["Some doc block comment"] + ) + + XCTAssertEqual( + Trivia(pieces: [ + .docBlockComment("/** Some doc block comment"), + .docBlockComment("* spread on many lines */"), + ]).commentValues, + [ + "Some doc block comment spread on many lines" + ] + ) + + XCTAssertEqual( + Trivia(pieces: [ + .docBlockComment("/** Some doc block comment"), + .docBlockComment("* spread on many lines"), + .docBlockComment("*/"), + ]).commentValues, + [ + "Some doc block comment spread on many lines" + ] + ) + + XCTAssertEqual( + Trivia(pieces: [ + .docBlockComment("/** Some doc block comment"), + .docBlockComment("* spread on many lines */"), + .docBlockComment("/** Another doc block comment */"), + ]).commentValues, + [ + "Some doc block comment spread on many lines", + "Another doc block comment", + ] + ) + + XCTAssertEqual( + Trivia(pieces: [ + .docBlockComment("/** Some doc block comment"), + .docBlockComment("* spread on many lines */"), + .newlines(2), + .docBlockComment("/** Another doc block comment */"), + ]).commentValues, + [ + "Some doc block comment spread on many lines", + "Another doc block comment", + ] + ) + + XCTAssertEqual( + Trivia(pieces: [ + .docBlockComment( + """ + /** + Some doc block comment + spread on many lines + */ + """ + ) + ]).commentValues, + ["Some doc block comment spread on many lines"] + ) + + XCTAssertEqual( + Trivia(pieces: [ + .docBlockComment( + """ + /** + * Some doc block comment + * spread on many lines + */ + """ + ) + ]).commentValues, + ["Some doc block comment spread on many lines"] + ) + + // MARK: Mixing comment styles + + XCTAssertEqual( + Trivia(pieces: [ + .docBlockComment( + """ + /** + * Some doc block comment + * // spread on many lines + * with a line comment + */ + """ + ) + ]).commentValues, + ["Some doc block comment // spread on many lines with a line comment"] + ) + + XCTAssertEqual( + Trivia(pieces: [ + .docBlockComment("/** Some doc block comment"), + .docBlockComment("* spread on many lines */"), + .newlines(2), + .docLineComment("/// Some doc line comment"), + .docLineComment("// Some line comment"), + .newlines(2), + .spaces(4), + .blockComment("/* Some block comment"), + .blockComment("* spread on many lines */"), + .newlines(2), + .docBlockComment("/** Another doc block comment */"), + ]).commentValues, + [ + "Some doc block comment spread on many lines", + "Some doc line comment", + "Some line comment", + "Some block comment spread on many lines", + "Another doc block comment", + ] + ) + + XCTAssertEqual( + Trivia(pieces: [ + .docBlockComment("/* Some block comment"), + .docLineComment("// A line comment in a block"), + .docBlockComment("* spread on many lines */"), + .newlines(2), + .blockComment("/** Some doc block comment"), + .docLineComment("/// A doc line comment in a block"), + .blockComment("* spread on"), + .blockComment("* many lines */"), + ]).commentValues, + [ + "Some block comment // A line comment in a block spread on many lines", + "Some doc block comment /// A doc line comment in a block spread on many lines", + ] + ) + } }